├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── adapters
├── salat-adapter
│ ├── build.sbt
│ └── src
│ │ └── main
│ │ ├── resources
│ │ └── reference.conf
│ │ └── scala
│ │ └── spray
│ │ └── oauth
│ │ └── adapters
│ │ └── salat
│ │ ├── SalatDataHandler.scala
│ │ ├── SprayOAuth2Support.scala
│ │ ├── models
│ │ ├── Code.scala
│ │ ├── Consumer.scala
│ │ ├── Role.scala
│ │ ├── Token.scala
│ │ └── User.scala
│ │ └── utils
│ │ ├── BaseDAO.scala
│ │ ├── MongoFactory.scala
│ │ └── package.scala
└── slick-adapter
│ ├── build.sbt
│ └── src
│ ├── main
│ ├── resources
│ │ └── reference.conf
│ └── scala
│ │ └── spray
│ │ └── oauth
│ │ └── adapters
│ │ └── slick
│ │ ├── SlickDataHandler.scala
│ │ ├── SprayOAuth2Support.scala
│ │ ├── models
│ │ ├── Code.scala
│ │ ├── Consumer.scala
│ │ ├── Role.scala
│ │ ├── Token.scala
│ │ └── User.scala
│ │ └── utils
│ │ └── BaseDAO.scala
│ └── test
│ ├── resources
│ └── application.conf
│ └── scala
│ ├── HelloSuite.scala
│ └── StackSpec.scala
├── build.sbt
├── core
├── build.sbt
└── src
│ └── main
│ ├── resources
│ └── reference.conf
│ └── scala
│ └── spray
│ └── oauth
│ ├── OAuth2DataHandler.scala
│ ├── OAuth2GrantHandler.scala
│ ├── adapters
│ └── inmemory
│ │ ├── InMemoryDataHandler.scala
│ │ ├── SprayOAuth2Support.scala
│ │ ├── models
│ │ ├── Code.scala
│ │ ├── Consumer.scala
│ │ ├── Role.scala
│ │ ├── Token.scala
│ │ └── User.scala
│ │ └── utils
│ │ ├── DAO.scala
│ │ ├── Entity.scala
│ │ └── Sequence.scala
│ ├── authentication
│ ├── ResourceAuthenticator.scala
│ └── SessionAuthenticator.scala
│ ├── directives
│ ├── OAuth2Directives.scala
│ ├── ResourceDirectives.scala
│ └── SessionDirectives.scala
│ ├── endpoints
│ ├── AuthorizeService.scala
│ ├── OAuth2Services.scala
│ └── TokenService.scala
│ ├── models
│ ├── AuthModel.scala
│ └── TokenModel.scala
│ ├── rejections
│ ├── AuthHandlerErrorRejection.scala
│ └── SecurePageRejection.scala
│ └── utils
│ ├── CSRFUtils.scala
│ ├── DefaultGrantHandler.scala
│ ├── HashUtils.scala
│ ├── OAuth2Parameters.scala
│ ├── OAuth2Utils.scala
│ └── TokenGenerator.scala
├── project
├── build.properties
└── plugins.sbt
└── samples
└── inmemory-webapp
├── build.sbt
└── src
└── main
├── resources
└── application.conf
└── scala
└── com
└── hasanozgan
└── demo
└── inmemory
├── Boot.scala
└── oauth2
├── OAuth2Actor.scala
├── routes
├── APIRoutes.scala
└── IndexRoutes.scala
└── utils
├── CustomRejectionHandler.scala
└── SecurePageRejection.scala
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | charset = utf-8
8 |
9 | [*.{scala,sbt}]
10 | indent_style = space
11 | indent_size = 2
12 |
13 | [*.xml]
14 | indent_style = space
15 | indent_size = 2
16 |
17 | [*.json]
18 | indent_style = space
19 | indent_size = 2
20 |
21 | [*.conf]
22 | indent_style = space
23 | indent_size = 4
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.class
3 | *.log
4 |
5 | # MacOSX
6 | .DS_Store
7 |
8 | # intellij idea
9 | .idea*
10 |
11 | # sbt specific
12 | project/credentials.sbt
13 | .cache/
14 | .history/
15 | .lib/
16 | dist/*
17 | target/
18 | lib_managed/
19 | src_managed/
20 | project/boot/
21 | project/plugins/project/
22 |
23 | # Scala-IDE specific
24 | .scala_dependencies
25 | .worksheet
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala:
3 | - 2.10.4
4 | script:
5 | - sbt "+ test"
6 | jdk:
7 | - oraclejdk7
8 | - openjdk7
9 | services:
10 | - mongodb
11 | notifications:
12 | email:
13 | - hasan@ozgan.net
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Spray OAuth2 Provider Library
2 | ===========
3 |
4 | Spray OAuth2 Provider Library
5 |
6 | [](https://travis-ci.org/hasanozgan/spray-oauth)
7 |
8 | ##### Road Map
9 | - Scala 2.11 migration Thanks @jmphilli
10 | - Write tests
11 | - Spray OAuth Rejectives
12 | - Write documentation
13 | - Salat Demo with Twirl Support
14 | - Slick Adapter
15 |
16 | ##### SBT
17 |
18 | ```scala
19 | resolvers += "Spray OAuth repo" at "https://oss.sonatype.org/content/repositories/snapshots"
20 |
21 | libraryDependencies ++= Seq(
22 | "com.hasanozgan" %% "spray-oauth" % "1.0-SNAPSHOT",
23 | "com.hasanozgan" %% "spray-oauth-salat-plugin" % "1.0-SNAPSHOT"
24 | )
25 | ```
26 |
27 | ##### Actor Support
28 | - SprayOAuth2Support for adapter support (salat, slick and in-memory)
29 | - OAuth2Services for defaultOAuth2Routes
30 |
31 | ```scala
32 | class OAuth2Actor extends Actor with SprayOAuth2Support with OAuth2Services with IndexRoutes with CustomRejectionHandler {
33 |
34 | // the HttpService trait defines only one abstract member, which
35 | // connects the services environment to the enclosing actor or test
36 | def actorRefFactory = context
37 |
38 | // this actor only runs our route, but you could add
39 | // other things here, like request stream processing
40 | // or timeout handling
41 | def receive = handleTimeouts orElse runRoute(handleRejections(myRejectionHandler)(handleExceptions(myExceptionHandler)(defaultOAuth2Routes ~ initRoutes)))
42 |
43 | def handleTimeouts: Receive = {
44 | case Timedout(x: HttpRequest) =>
45 | sender ! HttpResponse(InternalServerError, "Something is taking way too long.")
46 | }
47 |
48 | implicit def myExceptionHandler(implicit log: LoggingContext) =
49 | ExceptionHandler.apply {
50 | case e: Exception => {
51 | complete(InternalServerError, e.getMessage)
52 | }
53 |
54 | }
55 | }
56 | ```
57 |
58 | ##### Default Route Services
59 |
60 | ###### Token Route Service
61 | ```scala
62 | package spray.oauth.endpoints
63 |
64 | import spray.json.DefaultJsonProtocol
65 |
66 | import spray.routing.HttpService
67 | import spray.oauth.directives.OAuth2Directives
68 | import spray.oauth.models.TokenRequest
69 | import spray.http.StatusCodes
70 | import spray.oauth.models.TokenResponse.{ Token, Error, Code }
71 | import spray.httpx.SprayJsonSupport
72 |
73 | object MyJsonProtocol extends DefaultJsonProtocol {
74 | implicit val TokenFormat = jsonFormat5(Token)
75 | implicit val CodeFormat = jsonFormat1(Code)
76 | implicit val ErrorFormat = jsonFormat2(Error)
77 | }
78 |
79 | import MyJsonProtocol._
80 |
81 | /**
82 | * Created by hasanozgan on 03/06/14.
83 | */
84 | trait TokenService extends HttpService with SprayJsonSupport with OAuth2Directives {
85 |
86 | val defaultTokenRoutes =
87 | path("token") {
88 | fetchTokenRequest { request: TokenRequest =>
89 | grantHandler(request) {
90 | case error: Error => complete(error)
91 | case token: Token => complete(token)
92 | case code: Code => complete(code)
93 | }
94 | }
95 | }
96 | }
97 | ```
98 |
99 | ###### Auth Route Service
100 | ```scala
101 | package spray.oauth.endpoints
102 |
103 | import spray.routing.{ RequestContext, HttpService }
104 | import spray.oauth.directives.OAuth2Directives
105 | import spray.http.{ HttpCredentials, HttpHeader, HttpRequest, StatusCodes }
106 | import spray.oauth.models.AuthResponse.{ Error, Redirect, Approval }
107 | import spray.routing.authentication._
108 | import scala.concurrent.{ ExecutionContext, Future }
109 | import ExecutionContext.Implicits.global
110 | import spray.routing.authentication.UserPass
111 | import spray.oauth.models.{ ApprovalForm, AuthResponse, AuthRequest }
112 | import spray.oauth.{ AuthUser, AuthInfo }
113 | import spray.oauth.utils.OAuth2Parameters._
114 | import spray.oauth.utils.OAuth2Utils
115 | import spray.http._
116 | import MediaTypes._
117 |
118 | /**
119 | * Created by hasanozgan on 03/06/14.
120 | */
121 | trait AuthorizeService extends HttpService with OAuth2Directives {
122 |
123 | def myUserPassAuthenticator(userPass: Option[UserPass]): Future[Option[AuthUser]] = {
124 | Future {
125 | if (userPass.isDefined)
126 | dataHandler.getUser(userPass.get.user, userPass.get.pass)
127 | else None
128 | }
129 | }
130 |
131 | val defaultAuthorizeRoutes =
132 | path("authorize") {
133 | authenticate(BasicAuth(myUserPassAuthenticator _, realm = "secure site")) { user =>
134 | fetchAuthRequest(user) { request =>
135 | authHandler(request) {
136 | case AuthResponse.Approval(form) => respondWithMediaType(`text/html`) {
137 | complete(renderApprovalForm(form))
138 | }
139 | case AuthResponse.Redirect(uri) => redirect(uri, StatusCodes.TemporaryRedirect)
140 | case AuthResponse.Error(err, desc) => complete(s"ERROR: ${err}") //render("tpl/approvalPage", request)
141 | }
142 | }
143 | }
144 | }
145 | }
146 | }
147 | ```
148 |
149 | ###### Rest API Authenticator Support
150 |
151 | ```scala
152 | package com.hasanozgan.demo.inmemory.oauth2.routes
153 |
154 | import spray.oauth.adapters.inmemory.SprayOAuth2Support
155 | import spray.oauth.authentication.ResourceAuthenticator
156 | import spray.routing.HttpService
157 | import scala.concurrent.ExecutionContext.Implicits.global
158 |
159 | /**
160 | * Created by hasanozgan on 31/07/14.
161 | */
162 | trait ApiRoutes extends SprayOAuth2Support with ResourceAuthenticator with HttpService {
163 | val apiRoutes =
164 | path("user") {
165 | get {
166 | authenticate(tokenAuthenticator) { info =>
167 | authorize(allowedScopes(info, "membership", "membership.readonly")) {
168 | complete("Success")
169 | }
170 | }
171 | }
172 | }
173 | }
174 | ```
175 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/build.sbt:
--------------------------------------------------------------------------------
1 | name := "spray-oauth-salat-plugin"
2 |
3 | version := "1.0-SNAPSHOT"
4 |
5 |
6 |
7 | resolvers ++= Seq(
8 | "Spray repo" at "http://repo.spray.io/",
9 | "Novus Release Repository" at "http://repo.novus.com/releases/"
10 | )
11 |
12 | libraryDependencies ++= Seq(
13 | "io.spray" %% "spray-routing" % "1.2.0",
14 | "com.novus" %% "salat" % "1.9.9",
15 | "org.mongodb" %% "casbah" % "2.7.3",
16 | "com.github.nscala-time" %% "nscala-time" % "1.4.0",
17 | "io.spray" %% "spray-testkit" % "1.3.1" % "test"
18 | )
19 |
20 | seq(Revolver.settings: _*)
21 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | ##################################################
2 | # spray-oauth-salat-plugin Reference Config File #
3 | ##################################################
4 |
5 | # This is the reference config file that contains all the default settings.
6 | # Make your edits/overrides in your application.conf.
7 |
8 | spray.oauth2.datasource {
9 | uri = "mongodb://localhost:27017/oauth2"
10 | }
11 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/SalatDataHandler.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat
2 |
3 | import spray.oauth._
4 | import com.mongodb.casbah.Imports._
5 | import scala.Some
6 | import spray.oauth.adapters.salat.models.{ CodeDAO, TokenDAO, UserDAO, ConsumerDAO }
7 | import spray.oauth.models.GrantType
8 | import com.mongodb.casbah.Imports
9 |
10 | /**
11 | * Created with IntelliJ IDEA.
12 | * User: hasan.ozgan
13 | * Date: 6/3/14
14 | * Time: 8:50 AM
15 | * To change this template use File | Settings | File Templates.
16 | */
17 | object SalatDataHandler extends OAuth2DataHandler {
18 |
19 | override def findAuthInfoByRefreshToken(refreshToken: String): Option[AuthInfo] = {
20 | TokenDAO.findAuthInfoByRefreshToken(refreshToken)
21 | }
22 |
23 | override def deleteCode(code: String): Unit = {
24 | CodeDAO.deleteCode(code)
25 | }
26 |
27 | override def findAuthInfoByCode(code: String): Option[AuthInfo] = {
28 | CodeDAO.findAuthInfoByCode(code)
29 | }
30 |
31 | override def findAuthInfoByUser(clientId: String, user: AuthUser, grantType: GrantType.Value): Option[AuthInfo] = {
32 | TokenDAO.findUserToken(clientId, user, grantType)
33 | }
34 |
35 | override def findAuthInfoByClient(clientId: String): Option[AuthInfo] = {
36 | TokenDAO.findConsumerToken(clientId)
37 | }
38 |
39 | /*
40 | def findAuthInfoByConsumer(clientId: String, clientSecret: String, scope: Option[String]): Option[AuthInfo] = {
41 | ConsumerDAO.findWithCredentialsWithScope(clientId, clientSecret, scope) map { consumer =>
42 | val refreshable = ConsumerDAO.fetchGrantList(consumer).contains(GrantType.RefreshToken.toString)
43 | AuthInfo(None, Some(consumer.client_id), scope, None, refreshable)
44 | }
45 | }
46 |
47 | // FIXME: Design issue
48 | def findAuthInfoByUser(clientId: String, scope: Option[String], user: ObjectId): Option[AuthInfo[Imports.ObjectId]] = {
49 | TokenDAO.findByUser(clientId, user) map { token =>
50 | AuthInfo(Some(user), Some(clientId), token.toScopeString, None, token.refresh_token.isDefined)
51 | }
52 | }
53 |
54 | // FIXME: Design issue
55 | def findAuthInfoByUser(clientId: String, clientSecret: String, scope: Option[String], user: ObjectId): Option[AuthInfo[Imports.ObjectId]] = {
56 | ConsumerDAO.findWithCredentialsWithScope(clientId, clientSecret, scope) map { consumer =>
57 | val refreshable = ConsumerDAO.fetchGrantList(consumer).contains(GrantType.RefreshToken.toString)
58 | AuthInfo(Some(user), Some(consumer.client_id), scope, None, refreshable)
59 | }
60 | }
61 | */
62 |
63 | override def createAccessToken(authInfo: AuthInfo): Option[AccessToken] = {
64 | TokenDAO.createToken(authInfo).map { x => x.toAccessToken }
65 | }
66 |
67 | override def refreshAccessToken(authInfo: AuthInfo, refreshToken: String): Option[AccessToken] = {
68 | TokenDAO.findByAuthInfo(authInfo) match {
69 | case None => createAccessToken(authInfo)
70 | case Some(token) => TokenDAO.renewToken(authInfo, token).map { x => x.toAccessToken }
71 | }
72 | }
73 |
74 | override def createCode(authInfo: AuthInfo): Option[String] = {
75 | CodeDAO.createCode(authInfo).map { x => x.code.toString }
76 | }
77 |
78 | override def findAccessToken(authInfo: AuthInfo): Option[AccessToken] = {
79 | TokenDAO.findByAuthInfo(authInfo).map { x => x.toAccessToken }
80 | }
81 |
82 | override def findAuthInfoByAccessToken(token: String): Option[AuthInfo] = {
83 | TokenDAO.findAuthInfoByAccessToken(token)
84 | }
85 |
86 | override def getUser(username: String, password: String): Option[AuthUser] = {
87 | UserDAO.findWithCredentials(username, password).map(user => AuthUser(user.id.toString))
88 | }
89 |
90 | override def checkUserCredentials(username: String, password: String): Boolean = {
91 | getUser(username, password).isDefined
92 | }
93 |
94 | override def getClient(clientId: String, clientSecret: String): Option[AuthClient] = {
95 | ConsumerDAO.findWithCredentials(clientId, clientSecret) map { consumer =>
96 | AuthClient(consumer.client_id,
97 | ConsumerDAO.fetchGrantList(consumer).map(x => GrantType.convertFromString(x)),
98 | ConsumerDAO.fetchScopeList(consumer))
99 | }
100 | }
101 |
102 | override def getClient(clientId: String): Option[AuthClient] = {
103 | ConsumerDAO.findById(clientId) map { consumer =>
104 | AuthClient(consumer.client_id,
105 | ConsumerDAO.fetchGrantList(consumer).map(x => GrantType.convertFromString(x)),
106 | ConsumerDAO.fetchScopeList(consumer))
107 | }
108 | }
109 |
110 | override def checkConsumerCredentials(clientId: String, clientSecret: String): Boolean = {
111 | getClient(clientId, clientSecret).isDefined
112 | }
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/SprayOAuth2Support.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat
2 |
3 | import spray.oauth.{ OAuth2GrantHandler, OAuth2DataHandler }
4 | import spray.oauth.utils.DefaultGrantHandler
5 | import spray.oauth.endpoints.{ OAuth2Services }
6 | import com.mongodb.casbah.Imports._
7 |
8 | /**
9 | * Created by hasanozgan on 03/06/14.
10 | */
11 | trait SprayOAuth2Support {
12 |
13 | implicit def grantHandler: OAuth2GrantHandler = DefaultGrantHandler
14 |
15 | implicit def dataHandler: OAuth2DataHandler = SalatDataHandler
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/models/Code.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.models
2 |
3 | import com.novus.salat.global._
4 | import com.novus.salat.annotations._
5 | import com.mongodb.casbah.Imports._
6 | import com.github.nscala_time.time.Imports._
7 | import org.joda.time.PeriodType
8 | import spray.oauth.models.GrantType
9 | import spray.oauth.utils.OAuth2Parameters._
10 | import spray.oauth.utils.OAuth2Utils._
11 | import spray.oauth.utils.TokenGenerator
12 | import spray.oauth.adapters.salat.utils.BaseDAO
13 | import spray.oauth.{ AuthUser, AuthInfo }
14 |
15 | /**
16 | * Created with IntelliJ IDEA.
17 | * User: hasan.ozgan
18 | * Date: 4/21/14
19 | * Time: 9:44 AM
20 | * To change this template use File | Settings | File Templates.
21 | */
22 |
23 | case class Code(@Key("_id") id: ObjectId = new ObjectId,
24 | @Key("_fk_consumer") fk_consumer: ObjectId,
25 | @Key("_fk_user") fk_user: Option[ObjectId],
26 | scope: Option[String],
27 | code: String,
28 | token_refreshable: Boolean,
29 | redirect_uri: Option[String],
30 | ip_restriction: Option[String],
31 | created_on: DateTime = DateTime.now,
32 | deleted_on: DateTime = DateTime.now,
33 | expired_on: DateTime) {
34 |
35 | def expires_in = {
36 | if (expired_on >= DateTime.now) {
37 | val interval: Interval = new Interval(DateTime.now, expired_on)
38 | interval.toPeriod(PeriodType.seconds()).getSeconds
39 | } else 0
40 | }
41 |
42 | def toAuthInfo: AuthInfo = {
43 | val clientId = Some(fk_consumer.toString)
44 | AuthInfo(fk_user.map(x => AuthUser(x.toString)), clientId, scope, redirect_uri, token_refreshable, GrantType.AuthorizationCode, ip_restriction)
45 | }
46 |
47 | }
48 |
49 | object CodeDAO extends BaseDAO[Code]("codes") {
50 | def findAuthInfoByCode(code: String): Option[AuthInfo] = {
51 | findOne(MongoDBObject("code" -> code)).filter(x => x.expires_in > 0).map { x => x.toAuthInfo }
52 | }
53 |
54 | def deleteCode(code: String): Unit = {
55 | remove(MongoDBObject("code" -> code))
56 | }
57 |
58 | def createCode(info: AuthInfo): Option[Code] = {
59 | val clientId = info.clientId.map { x => new ObjectId(x) }.getOrElse(throw new Exception("ClientId Not Found"))
60 | val created_on = DateTime.now
61 | val expired_on = created_on + CODE_DURATION
62 | var code = Code(new ObjectId, clientId, info.user.map(x => new ObjectId(x.id)), info.scope, TokenGenerator.bearer(CODE_LENGTH), info.refreshable, info.redirectUri, info.remoteAddress, created_on, created_on, expired_on)
63 |
64 | try {
65 | this.save(code)
66 | Some(code)
67 | } catch {
68 | case e: Exception => None
69 | }
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/models/Consumer.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.models
2 |
3 | import com.novus.salat.global._
4 | import com.novus.salat.annotations._
5 | import com.mongodb.casbah.Imports._
6 | import com.github.nscala_time.time.Imports._
7 | import spray.oauth.utils.OAuth2Utils
8 | import spray.oauth.adapters.salat.utils.BaseDAO
9 |
10 | /**
11 | * Created with IntelliJ IDEA.
12 | * User: hasan.ozgan
13 | * Date: 3/7/14
14 | * Time: 10:44 AM
15 | * To change this template use File | Settings | File Templates.
16 | */
17 |
18 | case class Consumer(@Key("_id") id: ObjectId,
19 | @Key("_fk_role") fk_role: ObjectId,
20 | name: String,
21 | scopes: List[String] = Nil,
22 | grants: List[String] = Nil,
23 | site_url: Option[String],
24 | logo: Option[String],
25 | description: Option[String],
26 | callback_url: Option[String],
27 | client_secret: String,
28 | created_on: DateTime = DateTime.now,
29 | deleted_on: Option[DateTime] = None,
30 | deleted: Boolean = false) {
31 | def client_id = id.toString
32 | }
33 |
34 | object ConsumerDAO extends BaseDAO[Consumer]("consumers") {
35 | collection.ensureIndex("_idx_roles")
36 |
37 | private def mergedScopeList(consumer: Consumer, role: Role) = (consumer.scopes ::: role.scopes).distinct.toList
38 | private def mergedGrantList(consumer: Consumer, role: Role) = (consumer.grants ::: role.grants).distinct.toList
39 | private def getRole(consumer: Consumer, role: Option[Role]) = if (role.isEmpty) RoleDAO.findOneById(consumer.fk_role) else role
40 |
41 | def fetchScopeList(consumer: Consumer, role: Option[Role] = None) = {
42 | getRole(consumer, role) match {
43 | case Some(r) => mergedScopeList(consumer, r)
44 | case None => consumer.scopes
45 | }
46 | }
47 |
48 | def fetchGrantList(consumer: Consumer, role: Option[Role] = None) = {
49 | getRole(consumer, role) match {
50 | case Some(r) => mergedGrantList(consumer, r)
51 | case None => consumer.grants
52 | }
53 | }
54 |
55 | def findWithCredentials(client_id: String, client_secret: String): Option[Consumer] = {
56 | findOne(MongoDBObject("_id" -> new ObjectId(client_id), "client_secret" -> client_secret))
57 | }
58 |
59 | def findWithClientIdAndScope(client_id: String, scope: Option[String]): Option[Consumer] = {
60 | findById(client_id) match {
61 | case Some(consumer) => {
62 | val requestedScopes = OAuth2Utils.toScopeList(scope)
63 | requestedScopes diff ConsumerDAO.fetchScopeList(consumer) match {
64 | case Nil if requestedScopes.size > 0 => Some(consumer)
65 | case _ => None
66 | }
67 | }
68 | case None => None
69 | }
70 | }
71 |
72 | def findWithCredentialsWithScope(client_id: String, client_secret: String, scope: Option[String]): Option[Consumer] = {
73 | ConsumerDAO.findWithCredentials(client_id, client_secret) match {
74 | case Some(consumer) => {
75 | val requestedScopes = OAuth2Utils.toScopeList(scope)
76 | requestedScopes diff ConsumerDAO.fetchScopeList(consumer) match {
77 | case Nil if requestedScopes.size > 0 => Some(consumer)
78 | case _ => None
79 | }
80 | }
81 | case None => None
82 | }
83 | }
84 |
85 | def findConsumerScopes(client_id: String): List[String] = {
86 | ConsumerDAO.findById(client_id) match {
87 | case Some(consumer) => ConsumerDAO.fetchScopeList(consumer)
88 | case None => List.empty
89 |
90 | }
91 | }
92 |
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/models/Role.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.models
2 |
3 | import com.novus.salat.global._
4 | import com.novus.salat.annotations._
5 | import com.mongodb.casbah.Imports._
6 | import com.github.nscala_time.time.Imports._
7 | import spray.oauth.adapters.salat.utils.BaseDAO
8 | import scala.collection.JavaConverters
9 |
10 | /**
11 | * Created with IntelliJ IDEA.
12 | * User: hasan.ozgan
13 | * Date: 3/7/14
14 | * Time: 10:44 AM
15 | * To change this template use File | Settings | File Templates.
16 | */
17 | case class Role(@Key("_id") id: ObjectId,
18 | name: String,
19 | scopes: List[String],
20 | grants: List[String],
21 | created_on: DateTime = DateTime.now,
22 | deleted_on: Option[DateTime] = None,
23 | deleted: Boolean = false)
24 |
25 | object RoleDAO extends BaseDAO[Role]("roles")
26 |
27 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/models/Token.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.models
2 |
3 | import com.novus.salat.annotations._
4 | import com.mongodb.casbah.Imports._
5 | import com.github.nscala_time.time.Imports._
6 | import com.novus.salat.EnumStrategy
7 | import spray.oauth.adapters.salat.utils._
8 | import org.joda.time.{ PeriodType, ReadablePeriod, ReadableDuration }
9 | import spray.oauth.models.GrantType
10 | import spray.oauth.utils.{ OAuth2Utils, TokenGenerator }
11 | import spray.oauth.{ AuthUser, AuthInfo, AccessToken }
12 | import spray.oauth.utils.OAuth2Utils._
13 | import spray.oauth.utils.OAuth2Parameters._
14 | import com.mongodb.casbah.commons.TypeImports.ObjectId
15 |
16 | /**
17 | * Created with IntelliJ IDEA.
18 | * User: hasan.ozgan
19 | * Date: 3/7/14
20 | * Time: 10:46 AM
21 | * To change this template use File | Settings | File Templates.
22 | */
23 | case class Token(@Key("_id") id: ObjectId = new ObjectId,
24 | @Key("_fk_consumer") fk_consumer: ObjectId,
25 | @Key("_fk_user") fk_user: Option[ObjectId],
26 | scopes: List[String],
27 | token: String,
28 | refresh_token: Option[String],
29 | redirect_uri: Option[String],
30 | ip_restriction: Option[String],
31 | grant_type: String,
32 | token_type: TokenType.Value = TokenType.Bearer,
33 | created_on: DateTime = DateTime.now,
34 | updated_on: DateTime = DateTime.now,
35 | expired_on: DateTime) {
36 |
37 | def expires_in = {
38 | if (expired_on >= DateTime.now) {
39 | val interval: Interval = new Interval(DateTime.now, expired_on)
40 | interval.toPeriod(PeriodType.seconds()).getSeconds
41 | } else 0
42 | }
43 |
44 | def toScopeString: Option[String] = {
45 | if (!scopes.isEmpty) Some(scopes.mkString(" ")) else None
46 | }
47 |
48 | def toAccessToken: AccessToken = {
49 | AccessToken(token, refresh_token, TokenType.Bearer.toString, toScopeString, expires_in, updated_on.toDate)
50 | }
51 |
52 | def toAuthInfo: AuthInfo = {
53 | val clientId = Some(fk_consumer.toString)
54 | val refreshable = refresh_token.isDefined
55 | AuthInfo(fk_user.map(x => AuthUser(x.toString)), clientId, toScopeString, redirect_uri, refreshable, GrantType.convertFromString(grant_type), ip_restriction)
56 | }
57 | }
58 |
59 | object TokenDAO extends BaseDAO[Token]("tokens") {
60 |
61 | def findByAccessToken(access_token: String): Option[Token] = {
62 | findOne(MongoDBObject("token" -> access_token))
63 | }
64 |
65 | def findConsumerToken(clientId: String) = {
66 | findOne(MongoDBObject("_fk_consumer" -> new ObjectId(clientId), "_fk_user" -> MongoDBObject("$exists" -> "false"), "grant_type" -> GrantType.ClientCredentials.toString)) map { x => x.toAuthInfo }
67 | }
68 |
69 | def findUserToken(clientId: String, user: AuthUser, grantType: GrantType.Value): Option[AuthInfo] = {
70 | findOne(MongoDBObject("_fk_consumer" -> new ObjectId(clientId), "_fk_user" -> new ObjectId(user.id), "grant_type" -> grantType.toString)) map { x => x.toAuthInfo }
71 | }
72 |
73 | def getCurrentAccessToken(access_token: String): Option[Token] = {
74 | findByAccessToken(access_token).filter(x => x.expires_in > 0)
75 | }
76 |
77 | def findAuthInfoByAccessToken(access_token: String): Option[AuthInfo] = {
78 | getCurrentAccessToken(access_token) map { x => x.toAuthInfo }
79 | }
80 |
81 | def findByRefreshToken(refresh_token: String): Option[Token] = {
82 | findOne(MongoDBObject("refresh_token" -> refresh_token))
83 | }
84 |
85 | def findAuthInfoByRefreshToken(refresh_token: String): Option[AuthInfo] = {
86 | findByRefreshToken(refresh_token) map { x => x.toAuthInfo }
87 | }
88 |
89 | def findByAuthInfo(info: AuthInfo): Option[Token] = {
90 | try {
91 | val clientId = info.clientId.map { c => new ObjectId(c) }
92 | val userId = info.user.map(x => new ObjectId(x.id))
93 |
94 | findOne(MongoDBObject("_fk_consumer" -> clientId, "_fk_user" -> userId, "grant_type" -> info.grantType.toString))
95 | } catch {
96 | case ex: Exception => None
97 | }
98 | }
99 |
100 | def createToken(info: AuthInfo): Option[Token] = {
101 | findByAuthInfo(info) match {
102 | case Some(token) => renewToken(info, token)
103 | case None => newToken(info)
104 | }
105 | }
106 |
107 | def newToken(info: AuthInfo): Option[Token] = {
108 | val clientId = info.clientId.map { x => new ObjectId(x) }.getOrElse(throw new Exception("ClientId Not Found"))
109 | val access_token = TokenGenerator.bearer(TOKEN_LENGTH)
110 | val refresh_token: Option[String] = if (info.refreshable && info.remoteAddress.isEmpty) Some(TokenGenerator.bearer(REFRESH_TOKEN_LENGTH)) else None
111 | val created_on = DateTime.now
112 | val expired_on = created_on + TOKEN_DURATION
113 | var token = Token(new ObjectId, clientId, info.user.map(x => new ObjectId(x.id)), toScopeList(info.scope), access_token, refresh_token, info.redirectUri, info.remoteAddress, info.grantType.toString, TokenType.Bearer, created_on, created_on, expired_on)
114 |
115 | try {
116 | this.save(token)
117 | Some(token)
118 | } catch {
119 | case e: Exception => None
120 | }
121 | }
122 |
123 | def renewToken(info: AuthInfo, token: Token): Option[Token] = {
124 | val updated_on = DateTime.now
125 | val expired_on = updated_on + TOKEN_DURATION
126 | val access_token = TokenGenerator.bearer(TOKEN_LENGTH)
127 | val refresh_token: Option[String] = if (info.refreshable && info.remoteAddress.isEmpty) token.refresh_token else None
128 |
129 | val new_token = Token(token.id, token.fk_consumer, token.fk_user, toScopeList(info.scope), access_token, refresh_token, token.redirect_uri, info.remoteAddress, info.grantType.toString, token.token_type, token.created_on, updated_on, expired_on)
130 |
131 | try {
132 | this.save(new_token)
133 | Some(new_token)
134 | } catch {
135 | case e: Exception => {
136 | println(e)
137 | None
138 | }
139 | }
140 | }
141 | }
142 |
143 | @EnumAs(strategy = EnumStrategy.BY_VALUE)
144 | object TokenType extends Enumeration {
145 | val Bearer = Value("Bearer")
146 | }
147 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/models/User.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.models
2 |
3 | import com.novus.salat.annotations._
4 | import com.mongodb.casbah.Imports._
5 | import com.github.nscala_time.time.Imports._
6 | import spray.oauth.adapters.salat.utils._
7 | import spray.oauth.utils.HashUtils
8 | import sun.security.provider.MD5
9 | import java.security.MessageDigest
10 |
11 | /**
12 | * Created with IntelliJ IDEA.
13 | * User: hasan.ozgan
14 | * Date: 3/7/14
15 | * Time: 10:45 AM
16 | * To change this template use File | Settings | File Templates.
17 | */
18 | case class User(@Key("_id") id: ObjectId,
19 | user_id: String,
20 | username: String,
21 | password: String,
22 | created_on: DateTime = DateTime.now,
23 | deleted_on: Option[DateTime],
24 | deleted: Boolean = false)
25 |
26 | object UserDAO extends BaseDAO[User]("users") {
27 |
28 | def findWithCredentials(username: String, password: String) = {
29 | findOne(MongoDBObject("username" -> username, "password" -> HashUtils.md5(password)))
30 | }
31 |
32 | def create(user_id: String,
33 | username: String,
34 | password: String) = {
35 |
36 | this.insert(
37 | User(id = new ObjectId,
38 | user_id = user_id,
39 | username = username,
40 | password = HashUtils.md5(password),
41 | created_on = DateTime.now,
42 | deleted_on = None,
43 | deleted = false)
44 | )
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/utils/BaseDAO.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.utils
2 |
3 | import com.novus.salat.dao.SalatDAO
4 | import com.mongodb.casbah.Imports._
5 | import com.novus.salat.Context
6 |
7 | /**
8 | * Created by hasanozgan on 04/03/14.
9 | */
10 | class BaseDAO[T <: AnyRef](val coll: String)(implicit mot: Manifest[T], mid: Manifest[ObjectId], ctx: Context)
11 | extends SalatDAO[T, ObjectId](MongoFactory.getCollection(coll)) {
12 |
13 | collection.setWriteConcern(wc)
14 |
15 | def findAll: List[T] = {
16 | find(MongoDBObject.empty).toList
17 | }
18 |
19 | def removeAll = {
20 | collection.remove(MongoDBObject.empty)
21 | }
22 |
23 | def drop() {
24 | collection.drop()
25 | }
26 | /*
27 | def save(bulk: List[T], wc: WriteConcern = WriteConcern.Safe) {
28 | bulk.foreach {
29 | super.save(_, wc)
30 | }
31 | }
32 | */
33 | def findById(id: String): Option[T] = {
34 | findOneById(new ObjectId(id))
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/utils/MongoFactory.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat.utils
2 |
3 | import com.typesafe.config.ConfigFactory
4 | import com.mongodb.casbah.{ MongoDB, MongoURI, MongoCollection, MongoConnection }
5 |
6 | object MongoFactory {
7 |
8 | lazy val conf = ConfigFactory.load()
9 |
10 | def getCollection(collection: String): MongoCollection = {
11 | lazy val uri = MongoURI(conf.getString(s"spray.oauth2.datasource.uri"))
12 | lazy val connection = MongoConnection(uri)(uri.database.get)(collection)
13 | connection
14 | }
15 | }
--------------------------------------------------------------------------------
/adapters/salat-adapter/src/main/scala/spray/oauth/adapters/salat/utils/package.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.salat
2 |
3 | import com.mongodb.WriteConcern
4 | import com.novus.salat._
5 | import com.mongodb.casbah.Imports._
6 | import spray.oauth.AuthUser
7 |
8 | /**
9 | * Created with IntelliJ IDEA.
10 | * User: hasan.ozgan
11 | * Date: 6/3/14
12 | * Time: 9:02 AM
13 | * To change this template use File | Settings | File Templates.
14 | */
15 | package object utils {
16 |
17 | com.mongodb.casbah.commons.conversions.scala.RegisterConversionHelpers()
18 | com.mongodb.casbah.commons.conversions.scala.RegisterJodaTimeConversionHelpers()
19 |
20 | implicit val wc: WriteConcern = WriteConcern.SAFE
21 |
22 | implicit val ctx = new Context {
23 | val name = "When-Necessary-TypeHint-Context"
24 | override val typeHintStrategy = StringTypeHintStrategy(when = TypeHintFrequency.WhenNecessary, typeHint = TypeHint)
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/adapters/slick-adapter/build.sbt:
--------------------------------------------------------------------------------
1 | name := "spray-oauth-slick-plugin"
2 |
3 | version := "1.0-SNAPSHOT"
4 |
5 | resolvers ++= Seq(
6 | "Spray repo" at "http://repo.spray.io/",
7 | "Typesafe repo" at "http://repo.typesafe.com/typesafe/releases"
8 | )
9 |
10 | libraryDependencies ++= Seq(
11 | "io.spray" %% "spray-routing" % "1.3.1",
12 | "com.typesafe.akka" %% "akka-actor" % "2.3.5",
13 | "com.typesafe.slick" %% "slick" % "2.1.0",
14 | "com.typesafe.slick" %% "slick-extensions" % "2.1.0",
15 | "joda-time" % "joda-time" % "2.4",
16 | "org.joda" % "joda-convert" % "1.6",
17 | "com.github.tototoshi" %% "slick-joda-mapper" % "1.2.0",
18 | "com.h2database" % "h2" % "1.4.181" % "test",
19 | "org.scalatest" %% "scalatest" % "2.2.1" % "test",
20 | "io.spray" %% "spray-testkit" % "1.3.1" % "test",
21 | "com.typesafe.slick" %% "slick-testkit" % "2.1.0" % "test",
22 | "com.typesafe.akka" %% "akka-testkit" % "2.3.5" % "test"
23 | )
24 |
25 | seq(Revolver.settings: _*)
26 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | ##################################################
2 | # spray-oauth-salat-plugin Reference Config File #
3 | ##################################################
4 |
5 | # This is the reference config file that contains all the default settings.
6 | # Make your edits/overrides in your application.conf.
7 |
8 | spray.oauth2.datasource {
9 | uri = "jdbc:h2:mem:test1"
10 | driver = "org.h2.Driver"
11 | username = ""
12 | password = ""
13 | }
14 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/SlickDataHandler.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick
2 |
3 | import spray.oauth.adapters.slick.models.UserDAO
4 | import spray.oauth.models.GrantType
5 | import spray.oauth._
6 |
7 | /**
8 | * Created by hasanozgan on 04/08/14.
9 | */
10 | object SlickDataHandler extends OAuth2DataHandler {
11 | override def getUser(username: String, password: String): Option[AuthUser] = ???
12 |
13 | override def findAuthInfoByClient(clientId: String): Option[AuthInfo] = ???
14 |
15 | override def deleteCode(code: String): Unit = ???
16 |
17 | override def createAccessToken(authInfo: AuthInfo): Option[AccessToken] = ???
18 |
19 | override def getClient(clientId: String, clientSecret: String): Option[AuthClient] = ???
20 |
21 | override def getClient(clientId: String): Option[AuthClient] = ???
22 |
23 | override def createCode(authInfo: AuthInfo): Option[String] = ???
24 |
25 | override def findAuthInfoByRefreshToken(refreshToken: String): Option[AuthInfo] = ???
26 |
27 | override def refreshAccessToken(authInfo: AuthInfo, refreshToken: String): Option[AccessToken] = ???
28 |
29 | override def checkUserCredentials(username: String, password: String): Boolean = ???
30 |
31 | override def findAuthInfoByUser(clientId: String, user: AuthUser, grantType: GrantType.Value): Option[AuthInfo] = ???
32 |
33 | override def findAuthInfoByAccessToken(accessToken: String): Option[AuthInfo] = ???
34 |
35 | override def findAuthInfoByCode(code: String): Option[AuthInfo] = ???
36 |
37 | override def checkConsumerCredentials(clientId: String, clientSecret: String): Boolean = ???
38 |
39 | override def findAccessToken(info: AuthInfo): Option[AccessToken] = ???
40 | }
41 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/SprayOAuth2Support.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick
2 |
3 | import spray.oauth.utils.DefaultGrantHandler
4 | import spray.oauth.{ OAuth2DataHandler, OAuth2GrantHandler }
5 |
6 | /**
7 | * Created by hasanozgan on 04/08/14.
8 | */
9 | trait SprayOAuth2Support {
10 |
11 | implicit def grantHandler: OAuth2GrantHandler = DefaultGrantHandler
12 |
13 | implicit def dataHandler: OAuth2DataHandler = SlickDataHandler
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/models/Code.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick.models
2 |
3 | /**
4 | * Created by hasanozgan on 14/09/14.
5 | */
6 | class Code {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/models/Consumer.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick.models
2 |
3 | /**
4 | * Created by hasanozgan on 14/09/14.
5 | */
6 | class Consumer {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/models/Role.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick.models
2 |
3 | /**
4 | * Created by hasanozgan on 14/09/14.
5 | */
6 | class Role {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/models/Token.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick.models
2 |
3 | /**
4 | * Created by hasanozgan on 14/09/14.
5 | */
6 | class Token {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/models/User.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick.models
2 |
3 | import org.joda.time.DateTime
4 | import spray.oauth.adapters.slick.utils.BaseDAO
5 |
6 | import com.github.tototoshi.slick.JdbcJodaSupport._
7 | import scala.slick.driver.JdbcDriver.simple._
8 |
9 | /**
10 | * Created by hasanozgan on 14/09/14.
11 | */
12 | case class User(id: Long, user_id: String, username: String, password: String, created_on: DateTime, deleted_on: DateTime, deleted: Boolean)
13 |
14 | class Users(tag: Tag) extends Table[User](tag, "USERS") {
15 | def id = column[Long]("ID", O.PrimaryKey) // This is the primary key column
16 | def user_id = column[String]("USER_ID")
17 | def username = column[String]("USERNAME")
18 | def password = column[String]("PASSWORD")
19 | def created_on = column[DateTime]("CREATED_ON")
20 | def deleted_on = column[DateTime]("DELETED_ON")
21 | def deleted = column[Boolean]("DELETED")
22 | // Every table needs a * projection with the same type as the table's type parameter
23 | def * = (id, user_id, username, password, created_on, deleted_on, deleted) <> (User.tupled, User.unapply)
24 | }
25 |
26 | object UserDAO extends BaseDAO {
27 |
28 | val users = TableQuery[Users]
29 |
30 | def initial: Unit = {
31 | defaultDB withSession { implicit session =>
32 | users.ddl.create
33 | //users += (10, "123", "meddah", "123456", DateTime.now, DateTime.now, deleted = false)
34 | }
35 | }
36 |
37 | val querySalesByName = for {
38 | name <- Parameters[String]
39 | c <- users if c.username is name
40 | } yield c.password
41 |
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/main/scala/spray/oauth/adapters/slick/utils/BaseDAO.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.slick.utils
2 |
3 | import com.typesafe.config.ConfigFactory
4 |
5 | import scala.slick.jdbc.JdbcBackend.Database
6 | import scala.slick.lifted.{ AbstractTable, TableQuery }
7 |
8 | /**
9 | * Created by hasanozgan on 04/08/14.
10 | */
11 | class BaseDAO {
12 | lazy val conf = ConfigFactory.load()
13 | lazy val uri = conf.getString(s"spray.oauth2.datasource.uri")
14 | lazy val user = conf.getString(s"spray.oauth2.datasource.username")
15 | lazy val password = conf.getString(s"spray.oauth2.datasource.password")
16 | lazy val driver = conf.getString(s"spray.oauth2.datasource.driver")
17 |
18 | protected val defaultDB = Database.forURL(uri, user, password, driver = driver)
19 | }
20 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/test/resources/application.conf:
--------------------------------------------------------------------------------
1 | ##################################################
2 | # spray-oauth-salat-plugin Reference Config File #
3 | ##################################################
4 |
5 | # This is the reference config file that contains all the default settings.
6 | # Make your edits/overrides in your application.conf.
7 |
8 | spray.oauth2.datasource {
9 | uri = "jdbc:h2:mem:test1"
10 | driver = "org.h2.Driver"
11 | username = ""
12 | password = ""
13 | }
14 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/test/scala/HelloSuite.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by hasanozgan on 16/09/14.
3 | */
4 | import org.scalatest.FunSuite
5 |
6 | import scala.collection.mutable.Stack
7 |
8 | class HelloSuite extends FunSuite {
9 |
10 | test("the name is set correctly in constructor") {
11 | val stack = new Stack[Int]
12 | stack.push(1)
13 | stack.push(2)
14 | assert(stack.pop() == 2)
15 | assert(stack.pop() == 1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/adapters/slick-adapter/src/test/scala/StackSpec.scala:
--------------------------------------------------------------------------------
1 | import spray.oauth.adapters.slick.models.UserDAO
2 |
3 | import collection.mutable.Stack
4 | import org.scalatest._
5 |
6 | /**
7 | * Created by hasanozgan on 16/09/14.
8 | */
9 | class StackSpec extends FlatSpec with Matchers {
10 |
11 | "A Stack" should "pop values in last-in-first-out order" in {
12 | val stack = new Stack[Int]
13 | stack.push(1)
14 | stack.push(2)
15 | stack.pop() should be(2)
16 | stack.pop() should be(1)
17 |
18 | UserDAO.initial
19 | }
20 |
21 | it should "throw NoSuchElementException if an empty stack is popped" in {
22 | val emptyStack = new Stack[Int]
23 | a[NoSuchElementException] should be thrownBy {
24 | emptyStack.pop()
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import AssemblyKeys._
2 |
3 | lazy val commonSettings = Seq(
4 | organization := "com.hasanozgan",
5 | version := "1.0-SNAPSHOT",
6 | scalaVersion := "2.11.2",
7 | scalacOptions := Seq(
8 | "-unchecked",
9 | "-deprecation",
10 | "-encoding", "utf8",
11 | "-feature",
12 | "-language:postfixOps",
13 | "-language:implicitConversions",
14 | "-language:existentials"))
15 |
16 | lazy val settings = (
17 | commonSettings
18 | ++ scalariformSettings
19 | ++ org.scalastyle.sbt.ScalastylePlugin.Settings
20 | ++ assemblySettings)
21 |
22 | lazy val publishSettings = Seq(
23 | publishMavenStyle := true,
24 | publishArtifact in Test := false,
25 | publishTo := {
26 | val nexus = "https://oss.sonatype.org/"
27 | if (isSnapshot.value)
28 | Some("snapshots" at nexus + "content/repositories/snapshots")
29 | else
30 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
31 | },
32 | pomExtra := (
33 | http://github.com/hasanozgan/spray-oauth2
34 |
35 |
36 | Apache 2
37 | http://www.apache.org/licenses/LICENSE-2.0
38 | repo
39 |
40 |
41 |
42 | git@github.com:hasanozgan/spray-oauth2.git
43 | scm:git@github.com:hasanozgan/spray-oauth2.git
44 |
45 |
46 |
47 | hasanozgan
48 | Hasan Ozgan
49 | http://github.com/hasanozgan
50 |
51 | ))
52 |
53 | lazy val root = project.in( file(".") ).aggregate(core, salatAdapter, slickAdapter, demo)
54 |
55 | lazy val core = project.in(file("core"))
56 | .settings(settings: _*)
57 | .settings(publishSettings: _*)
58 | .settings(test in assembly := {})
59 | .settings(testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"))
60 |
61 | lazy val salatAdapter = project.in(file("adapters/salat-adapter"))
62 | .dependsOn(core)
63 | .settings(settings: _*)
64 | .settings(publishSettings: _*)
65 | .settings(test in assembly := {})
66 | .settings(testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"))
67 |
68 | lazy val slickAdapter = project.in(file("adapters/slick-adapter"))
69 | .dependsOn(core)
70 | .settings(settings: _*)
71 | .settings(test in assembly := {})
72 | .settings(testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"))
73 |
74 | lazy val demo = project.in(file("samples/inmemory-webapp"))
75 | .dependsOn(core)
76 | .settings(settings: _*)
77 | .settings(test in assembly := {})
78 | .settings(testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-oDF"))
79 |
--------------------------------------------------------------------------------
/core/build.sbt:
--------------------------------------------------------------------------------
1 | name := "spray-oauth"
2 |
3 | version := "1.0-SNAPSHOT"
4 |
5 |
6 | resolvers ++= Seq(
7 | "Spray repo" at "http://repo.spray.io/",
8 | "Typesafe repo" at "http://repo.typesafe.com/typesafe/releases"
9 | )
10 |
11 | libraryDependencies ++= Seq(
12 | "io.spray" %% "spray-routing" % "1.3.1",
13 | "io.spray" %% "spray-json" % "1.3.1",
14 | "com.typesafe.akka" %% "akka-actor" % "2.3.5",
15 | "com.github.nscala-time" %% "nscala-time" % "1.4.0",
16 | "org.scalatest" %% "scalatest" % "2.2.1" % "test",
17 | "io.spray" %% "spray-testkit" % "1.3.1" % "test",
18 | "com.typesafe.akka" %% "akka-testkit" % "2.3.5" % "test"
19 | )
20 |
21 | seq(Revolver.settings: _*)
22 |
--------------------------------------------------------------------------------
/core/src/main/resources/reference.conf:
--------------------------------------------------------------------------------
1 | #####################################
2 | # spray-oauth Reference Config File #
3 | #####################################
4 |
5 | # This is the reference config file that contains all the default settings.
6 | # Make your edits/overrides in your application.conf.
7 |
8 | spray.oauth2 {
9 | scope-separator = " "
10 | token-duration = 1h
11 | token-length = 32
12 | refresh-token-length = 48
13 | consumer-secret-length = 32
14 | show-scope = true
15 | code-length = 32
16 | code-duration = 5m
17 | secret = "eTKsUMM7bWTQ22TSvMMuiUgPexBixg9IA7JqhrDPQPt5pbbjqcPhpszggU2glAJ"
18 |
19 |
20 | approval-form {
21 | csrf-enabled = true
22 | csrf-token-key = "csrf_token"
23 | prefix-for-scope-key = "scope:"
24 | }
25 |
26 | application {
27 | session-key = "SessionID"
28 | session-cookie-domain = ""
29 | }
30 |
31 | resource {
32 | headers {
33 | versioning = "X-API-Version"
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/OAuth2DataHandler.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth
2 |
3 | import java.util.Date
4 |
5 | import spray.oauth.models.GrantType
6 | import spray.oauth.utils.OAuth2Utils
7 |
8 | case class AuthClient(id: String, grantTypes: List[GrantType.Value], scopes: List[String]) {
9 | def hasScope(scope: Option[String]): Boolean = {
10 | hasScope(OAuth2Utils.toScopeList(scope))
11 | }
12 |
13 | def hasScope(scope: List[String]): Boolean = {
14 | (scope diff this.scopes).isEmpty
15 | }
16 |
17 | def hasGrant(grantType: GrantType.Value): Boolean = {
18 | grantTypes.exists(x => x.equals(grantType))
19 | }
20 | }
21 |
22 | case class AuthUser(id: String)
23 |
24 | case class AccessToken(token: String, refreshToken: Option[String], tokenType: String, scope: Option[String], expiresIn: Long, createdAt: Date)
25 |
26 | case class AuthInfo(user: Option[AuthUser], clientId: Option[String], scope: Option[String], redirectUri: Option[String], refreshable: Boolean, grantType: GrantType.Value, remoteAddress: Option[String] = None)
27 |
28 | trait OAuth2DataHandler {
29 |
30 | /* Check Credentials */
31 |
32 | def getUser(username: String, password: String): Option[AuthUser]
33 |
34 | def getClient(clientId: String, clientSecret: String): Option[AuthClient]
35 |
36 | def getClient(clientId: String): Option[AuthClient]
37 |
38 | def checkUserCredentials(username: String, password: String): Boolean
39 |
40 | def checkConsumerCredentials(clientId: String, clientSecret: String): Boolean
41 |
42 | /* Authorization Code */
43 |
44 | def createCode(authInfo: AuthInfo): Option[String]
45 |
46 | def deleteCode(code: String): Unit
47 |
48 | def findAuthInfoByCode(code: String): Option[AuthInfo]
49 |
50 | /* Auth Info Methods */
51 |
52 | /* Access Token */
53 |
54 | def refreshAccessToken(authInfo: AuthInfo, refreshToken: String): Option[AccessToken]
55 |
56 | def createAccessToken(authInfo: AuthInfo): Option[AccessToken]
57 |
58 | def findAccessToken(info: AuthInfo): Option[AccessToken]
59 |
60 | def findAuthInfoByAccessToken(accessToken: String): Option[AuthInfo]
61 |
62 | def findAuthInfoByRefreshToken(refreshToken: String): Option[AuthInfo]
63 |
64 | def findAuthInfoByUser(clientId: String, user: AuthUser, grantType: GrantType.Value): Option[AuthInfo]
65 |
66 | def findAuthInfoByClient(clientId: String): Option[AuthInfo]
67 |
68 | /*
69 |
70 | def findAccessToken(token: String): Option[AccessToken]
71 |
72 | def findAuthInfoByAccessToken(accessToken: AccessToken): Option[AuthInfo[U]]
73 | */
74 |
75 | /* Helper Methods */
76 |
77 | def isAccessTokenExpired(accessToken: AccessToken): Boolean = {
78 | val now = System.currentTimeMillis()
79 | (accessToken.createdAt.getTime + accessToken.expiresIn * 1000) <= now
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/OAuth2GrantHandler.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth
2 |
3 | import spray.oauth.models.{ AuthRequest, TokenRequest }
4 | import spray.oauth.models.TokenResponse.TokenResponse
5 |
6 | /**
7 | * Created with IntelliJ IDEA.
8 | * User: hasan.ozgan
9 | * Date: 4/16/14
10 | * Time: 4:13 PM
11 | * To change this template use File | Settings | File Templates.
12 | */
13 | trait OAuth2GrantHandler {
14 |
15 | def authorizationCode(authInfo: AuthInfo)(implicit dataHandler: OAuth2DataHandler): TokenResponse
16 |
17 | def implicitToken(authInfo: AuthInfo)(implicit dataHandler: OAuth2DataHandler): TokenResponse
18 |
19 | def authorizationCode(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse
20 |
21 | def refreshToken(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse
22 |
23 | def clientCredentials(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse
24 |
25 | def password(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/InMemoryDataHandler.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory
2 |
3 | import spray.oauth.adapters.inmemory.models.{ ConsumerDAO, UserDAO, CodeDAO, TokenDAO }
4 | import spray.oauth.models.GrantType
5 | import spray.oauth._
6 |
7 | /**
8 | * Created by hasanozgan on 03/06/14.
9 | */
10 | private object InMemoryDataHandler extends OAuth2DataHandler {
11 |
12 | override def findAuthInfoByRefreshToken(refreshToken: String): Option[AuthInfo] = {
13 | TokenDAO.findAuthInfoByRefreshToken(refreshToken)
14 | }
15 |
16 | override def deleteCode(code: String): Unit = {
17 | CodeDAO.deleteCode(code)
18 | }
19 |
20 | override def findAuthInfoByCode(code: String): Option[AuthInfo] = {
21 | CodeDAO.findAuthInfoByCode(code)
22 | }
23 |
24 | override def findAuthInfoByUser(clientId: String, user: AuthUser, grantType: GrantType.Value): Option[AuthInfo] = {
25 | TokenDAO.findUserToken(clientId, user, grantType)
26 | }
27 |
28 | override def findAuthInfoByClient(clientId: String): Option[AuthInfo] = {
29 | TokenDAO.findConsumerToken(clientId)
30 | }
31 |
32 | override def createAccessToken(authInfo: AuthInfo): Option[AccessToken] = {
33 | TokenDAO.createToken(authInfo).map { x => x.toAccessToken }
34 | }
35 |
36 | override def refreshAccessToken(authInfo: AuthInfo, refreshToken: String): Option[AccessToken] = {
37 | TokenDAO.findByAuthInfo(authInfo) match {
38 | case None => createAccessToken(authInfo)
39 | case Some(token) => TokenDAO.renewToken(authInfo, token).map { x => x.toAccessToken }
40 | }
41 | }
42 |
43 | override def createCode(authInfo: AuthInfo): Option[String] = {
44 | CodeDAO.createCode(authInfo).map { x => x.code.toString }
45 | }
46 |
47 | override def findAccessToken(authInfo: AuthInfo): Option[AccessToken] = {
48 | TokenDAO.findByAuthInfo(authInfo).map { x => x.toAccessToken }
49 | }
50 |
51 | override def findAuthInfoByAccessToken(token: String): Option[AuthInfo] = {
52 | TokenDAO.findAuthInfoByAccessToken(token)
53 | }
54 |
55 | override def getUser(username: String, password: String): Option[AuthUser] = {
56 | UserDAO.findWithCredentials(username, password).map(user => AuthUser(user.id.toString))
57 | }
58 |
59 | override def checkUserCredentials(username: String, password: String): Boolean = {
60 | getUser(username, password).isDefined
61 | }
62 |
63 | override def getClient(clientId: String, clientSecret: String): Option[AuthClient] = {
64 | ConsumerDAO.findWithCredentials(clientId, clientSecret) map { consumer =>
65 | AuthClient(consumer.client_id,
66 | ConsumerDAO.fetchGrantList(consumer).map(x => GrantType.convertFromString(x)),
67 | ConsumerDAO.fetchScopeList(consumer))
68 | }
69 | }
70 |
71 | override def getClient(clientId: String): Option[AuthClient] = {
72 | ConsumerDAO.findBy(p => p.client_id.equals(clientId)) map { consumer =>
73 | AuthClient(consumer.client_id,
74 | ConsumerDAO.fetchGrantList(consumer).map(x => GrantType.convertFromString(x)),
75 | ConsumerDAO.fetchScopeList(consumer))
76 | }
77 | }
78 |
79 | override def checkConsumerCredentials(clientId: String, clientSecret: String): Boolean = {
80 | getClient(clientId, clientSecret).isDefined
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/SprayOAuth2Support.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory
2 |
3 | import spray.oauth.{ OAuth2GrantHandler, OAuth2DataHandler }
4 | import spray.oauth.utils.DefaultGrantHandler
5 | import spray.oauth.endpoints.{ OAuth2Services, AuthorizeService, TokenService }
6 |
7 | /**
8 | * Created by hasanozgan on 03/06/14.
9 | */
10 | trait SprayOAuth2Support {
11 |
12 | implicit def grantHandler: OAuth2GrantHandler = DefaultGrantHandler
13 |
14 | implicit def dataHandler: OAuth2DataHandler = InMemoryDataHandler
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/models/Code.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.models
2 |
3 | import com.github.nscala_time.time.Imports._
4 | import org.joda.time.PeriodType
5 | import spray.oauth.adapters.inmemory.utils.{ Entity, DAO, Sequence }
6 | import spray.oauth.models.GrantType
7 | import spray.oauth.utils.OAuth2Parameters._
8 | import spray.oauth.utils.TokenGenerator
9 | import spray.oauth.{ AuthUser, AuthInfo }
10 |
11 | /**
12 | * Created with IntelliJ IDEA.
13 | * User: hasan.ozgan
14 | * Date: 4/21/14
15 | * Time: 9:44 AM
16 | * To change this template use File | Settings | File Templates.
17 | */
18 |
19 | case class Code(
20 | id: Long,
21 | fk_consumer: Long,
22 | fk_user: Option[Long],
23 | scope: Option[String],
24 | code: String,
25 | token_refreshable: Boolean,
26 | redirect_uri: Option[String],
27 | ip_restriction: Option[String],
28 | created_on: DateTime = DateTime.now,
29 | deleted_on: DateTime = DateTime.now,
30 | expired_on: DateTime) extends Entity(id) {
31 |
32 | def expires_in = {
33 | if (expired_on >= DateTime.now) {
34 | val interval: Interval = new Interval(DateTime.now, expired_on)
35 | interval.toPeriod(PeriodType.seconds()).getSeconds
36 | } else 0
37 | }
38 |
39 | def toAuthInfo: AuthInfo = {
40 | val clientId = Some(fk_consumer.toString)
41 | AuthInfo(fk_user.map(x => AuthUser(x.toString)), clientId, scope, redirect_uri, token_refreshable, GrantType.AuthorizationCode, ip_restriction)
42 | }
43 | }
44 |
45 | object CodeDAO extends DAO[Code] {
46 | def findAuthInfoByCode(code: String): Option[AuthInfo] = {
47 | findOneByCode(code).filter(x => x.expires_in > 0).map { x => x.toAuthInfo }
48 | }
49 |
50 | def findOneByCode(code: String): Option[Code] = {
51 | findBy(p => p.code.equals(code))
52 | }
53 |
54 | def deleteCode(code: String): Unit = {
55 | val found = findBy(p => p.code.equals(code))
56 | if (found.nonEmpty) {
57 | remove(found.get)
58 | }
59 | }
60 |
61 | def createCode(info: AuthInfo): Option[Code] = {
62 | val clientId = info.clientId.map { x => x.toLong }.getOrElse(throw new Exception("ClientId Not Found"))
63 | val created_on = DateTime.now
64 |
65 | val expired_on = created_on + CODE_DURATION
66 | var code = Code(Sequence.nextId, clientId, info.user.map(x => x.id.toLong), info.scope, TokenGenerator.bearer(CODE_LENGTH), info.refreshable, info.redirectUri, info.remoteAddress, created_on, created_on, expired_on)
67 |
68 | try {
69 | this.save(code)
70 | Some(code)
71 | } catch {
72 | case e: Exception => None
73 | }
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/models/Consumer.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.models
2 |
3 | import com.github.nscala_time.time.Imports._
4 | import spray.oauth.adapters.inmemory.utils.{ Entity, DAO }
5 | import spray.oauth.utils.OAuth2Utils
6 |
7 | /**
8 | * Created with IntelliJ IDEA.
9 | * User: hasan.ozgan
10 | * Date: 3/7/14
11 | * Time: 10:44 AM
12 | * To change this template use File | Settings | File Templates.
13 | */
14 |
15 | case class Consumer(id: Long,
16 | fk_role: Long,
17 | name: String,
18 | scopes: List[String] = Nil,
19 | grants: List[String] = Nil,
20 | site_url: Option[String],
21 | logo: Option[String],
22 | description: Option[String],
23 | callback_url: Option[String],
24 | client_secret: String,
25 | created_on: DateTime = DateTime.now,
26 | deleted_on: Option[DateTime] = None,
27 | deleted: Boolean = false) extends Entity(id) {
28 |
29 | def client_id = id.toString
30 | }
31 |
32 | object ConsumerDAO extends DAO[Consumer] {
33 |
34 | private def mergedScopeList(consumer: Consumer, role: Role) = (consumer.scopes ::: role.scopes).distinct.toList
35 | private def mergedGrantList(consumer: Consumer, role: Role) = (consumer.grants ::: role.grants).distinct.toList
36 | private def getRole(consumer: Consumer, role: Option[Role]) = if (role.isEmpty) RoleDAO.findBy(p => p.id == consumer.fk_role) else role
37 |
38 | def fetchScopeList(consumer: Consumer, role: Option[Role] = None) = {
39 | getRole(consumer, role) match {
40 | case Some(r) => mergedScopeList(consumer, r)
41 | case None => consumer.scopes
42 | }
43 | }
44 |
45 | def fetchGrantList(consumer: Consumer, role: Option[Role] = None) = {
46 | getRole(consumer, role) match {
47 | case Some(r) => mergedGrantList(consumer, r)
48 | case None => consumer.grants
49 | }
50 | }
51 |
52 | def findWithCredentials(client_id: String, client_secret: String): Option[Consumer] = {
53 | findBy(p => p.client_id.equals(client_id) && p.client_secret.equals(client_secret))
54 | }
55 |
56 | def findWithClientIdAndScope(client_id: String, scope: Option[String]): Option[Consumer] = {
57 | findBy(p => p.client_id.equals(client_id)) match {
58 | case Some(consumer) => {
59 | val requestedScopes = OAuth2Utils.toScopeList(scope)
60 | requestedScopes diff ConsumerDAO.fetchScopeList(consumer) match {
61 | case Nil if requestedScopes.size > 0 => Some(consumer)
62 | case _ => None
63 | }
64 | }
65 | case None => None
66 | }
67 | }
68 |
69 | def findWithCredentialsWithScope(client_id: String, client_secret: String, scope: Option[String]): Option[Consumer] = {
70 | ConsumerDAO.findWithCredentials(client_id, client_secret) match {
71 | case Some(consumer) => {
72 | val requestedScopes = OAuth2Utils.toScopeList(scope)
73 | requestedScopes diff ConsumerDAO.fetchScopeList(consumer) match {
74 | case Nil if requestedScopes.size > 0 => Some(consumer)
75 | case _ => None
76 | }
77 | }
78 | case None => None
79 | }
80 | }
81 |
82 | def findConsumerScopes(client_id: String): List[String] = {
83 | ConsumerDAO.findBy(p => p.client_id.equals(client_id)) match {
84 | case Some(consumer) => {
85 |
86 | ConsumerDAO.fetchScopeList(consumer)
87 | }
88 | case None => List.empty
89 |
90 | }
91 | }
92 |
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/models/Role.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.models
2 |
3 | import com.github.nscala_time.time.Imports._
4 | import spray.oauth.adapters.inmemory.utils.{ Entity, DAO }
5 | import scala.collection.JavaConverters
6 |
7 | /**
8 | * Created with IntelliJ IDEA.
9 | * User: hasan.ozgan
10 | * Date: 3/7/14
11 | * Time: 10:44 AM
12 | * To change this template use File | Settings | File Templates.
13 | */
14 | case class Role(id: Long,
15 | name: String,
16 | scopes: List[String],
17 | grants: List[String],
18 | created_on: DateTime = DateTime.now,
19 | deleted_on: Option[DateTime] = None,
20 | deleted: Boolean = false) extends Entity(id) {
21 | }
22 |
23 | object RoleDAO extends DAO[Role]
24 |
25 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/models/Token.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.models
2 |
3 | import com.github.nscala_time.time.Imports._
4 | import spray.oauth.adapters.inmemory.utils.{ Sequence, Entity, DAO }
5 | import org.joda.time.PeriodType
6 | import spray.oauth.models.GrantType
7 | import spray.oauth.utils.TokenGenerator
8 | import spray.oauth.{ AuthUser, AuthInfo, AccessToken }
9 | import spray.oauth.utils.OAuth2Utils._
10 | import spray.oauth.utils.OAuth2Parameters._
11 |
12 | /**
13 | * Created with IntelliJ IDEA.
14 | * User: hasan.ozgan
15 | * Date: 3/7/14
16 | * Time: 10:46 AM
17 | * To change this template use File | Settings | File Templates.
18 | */
19 | case class Token(id: Long = 0,
20 | fk_consumer: Long,
21 | fk_user: Option[Long],
22 | scopes: List[String],
23 | token: String,
24 | refresh_token: Option[String],
25 | redirect_uri: Option[String],
26 | ip_restriction: Option[String],
27 | grant_type: String,
28 | token_type: TokenType.Value = TokenType.Bearer,
29 | created_on: DateTime = DateTime.now,
30 | updated_on: DateTime = DateTime.now,
31 | expired_on: DateTime) extends Entity(id) {
32 |
33 | def expires_in = {
34 | if (expired_on >= DateTime.now) {
35 | val interval: Interval = new Interval(DateTime.now, expired_on)
36 | interval.toPeriod(PeriodType.seconds()).getSeconds
37 | } else 0
38 | }
39 |
40 | def toScopeString: Option[String] = {
41 | if (scopes.nonEmpty) Some(scopes.mkString(" ")) else None
42 | }
43 |
44 | def toAccessToken: AccessToken = {
45 | AccessToken(token, refresh_token, TokenType.Bearer.toString, toScopeString, expires_in, updated_on.toDate)
46 | }
47 |
48 | def toAuthInfo: AuthInfo = {
49 | val clientId = Some(fk_consumer.toString)
50 | val refreshable = refresh_token.isDefined
51 | AuthInfo(fk_user.map(x => AuthUser(x.toString)), clientId, toScopeString, redirect_uri, refreshable, GrantType.convertFromString(grant_type), ip_restriction)
52 | }
53 | }
54 |
55 | object TokenDAO extends DAO[Token] {
56 |
57 | def findByAccessToken(access_token: String): Option[Token] = {
58 | findBy(p => p.token.equals(access_token))
59 | }
60 |
61 | def findConsumerToken(clientId: String) = {
62 | findBy(p => (p.fk_consumer == clientId.toLong) && !p.fk_user.isDefined && GrantType.ClientCredentials.toString.equals(p.grant_type)) map { x => x.toAuthInfo }
63 | }
64 |
65 | def findUserToken(clientId: String, user: AuthUser, grantType: GrantType.Value): Option[AuthInfo] = {
66 | findBy(p => (p.fk_consumer == clientId.toLong) && p.fk_user.exists(x => x == user.id.toLong) && GrantType.ClientCredentials.toString.equals(p.grant_type)) map { x => x.toAuthInfo }
67 | }
68 |
69 | def getCurrentAccessToken(access_token: String): Option[Token] = {
70 | findByAccessToken(access_token).filter(x => x.expires_in > 0)
71 | }
72 |
73 | def findAuthInfoByAccessToken(access_token: String): Option[AuthInfo] = {
74 | getCurrentAccessToken(access_token) map { x => x.toAuthInfo }
75 | }
76 |
77 | def findByRefreshToken(refresh_token: String): Option[Token] = {
78 | findBy(p => p.refresh_token.exists(x => x.equals(refresh_token)))
79 | }
80 |
81 | def findAuthInfoByRefreshToken(refresh_token: String): Option[AuthInfo] = {
82 | findByRefreshToken(refresh_token) map { x => x.toAuthInfo }
83 | }
84 |
85 | def findByAuthInfo(info: AuthInfo): Option[Token] = {
86 | try {
87 | val clientId = info.clientId.fold(0L) { c => c.toLong }
88 | val userId = info.user.fold(0L) { x => x.id.toLong }
89 | findBy(p => p.fk_consumer == clientId && p.fk_user.exists(x => x == userId) && p.grant_type.equals(info.grantType.toString))
90 | } catch {
91 | case ex: Exception => None
92 | }
93 | }
94 |
95 | def createToken(info: AuthInfo): Option[Token] = {
96 | findByAuthInfo(info) match {
97 | case Some(token) => renewToken(info, token)
98 | case None => newToken(info)
99 | }
100 | }
101 |
102 | def newToken(info: AuthInfo): Option[Token] = {
103 | val clientId = info.clientId.map { x => x.toLong }.getOrElse(throw new Exception("ClientId Not Found"))
104 | val access_token = TokenGenerator.bearer(TOKEN_LENGTH)
105 | val refresh_token: Option[String] = if (info.refreshable && info.remoteAddress.isEmpty) Some(TokenGenerator.bearer(REFRESH_TOKEN_LENGTH)) else None
106 | val created_on = DateTime.now
107 | val expired_on = created_on + TOKEN_DURATION
108 | var token = Token(Sequence.nextId, clientId, info.user.map(x => x.id.toLong), toScopeList(info.scope), access_token, refresh_token, info.redirectUri, info.remoteAddress, info.grantType.toString, TokenType.Bearer, created_on, created_on, expired_on)
109 |
110 | try {
111 | this.save(token)
112 | Some(token)
113 | } catch {
114 | case e: Exception => None
115 | }
116 | }
117 |
118 | def renewToken(info: AuthInfo, token: Token): Option[Token] = {
119 | val updated_on = DateTime.now
120 | val expired_on = updated_on + TOKEN_DURATION
121 | val access_token = TokenGenerator.bearer(TOKEN_LENGTH)
122 | val refresh_token: Option[String] = if (info.refreshable && info.remoteAddress.isEmpty) token.refresh_token else None
123 |
124 | val new_token = Token(token.id, token.fk_consumer, token.fk_user, toScopeList(info.scope), access_token, refresh_token, token.redirect_uri, info.remoteAddress, info.grantType.toString, token.token_type, token.created_on, updated_on, expired_on)
125 |
126 | try {
127 | this.save(new_token)
128 | Some(new_token)
129 | } catch {
130 | case e: Exception =>
131 | None
132 | }
133 | }
134 | }
135 |
136 | object TokenType extends Enumeration {
137 | val Bearer = Value("Bearer")
138 | }
139 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/models/User.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.models
2 |
3 | import com.github.nscala_time.time.Imports._
4 | import spray.oauth.adapters.inmemory.utils.{ Entity, DAO, Sequence }
5 | import spray.oauth.utils.HashUtils
6 |
7 | /**
8 | * Created with IntelliJ IDEA.
9 | * User: hasan.ozgan
10 | * Date: 3/7/14
11 | * Time: 10:45 AM
12 | * To change this template use File | Settings | File Templates.
13 | */
14 | case class User(id: Long,
15 | user_id: String,
16 | username: String,
17 | password: String,
18 | created_on: DateTime = DateTime.now,
19 | deleted_on: Option[DateTime],
20 | deleted: Boolean = false) extends Entity(id) {
21 | }
22 |
23 | object UserDAO extends DAO[User] {
24 |
25 | def findWithCredentials(username: String, password: String) = {
26 | findBy(x => x.username.equals(username) && x.password.equals(HashUtils.md5(password)))
27 | }
28 |
29 | def create(user_id: String,
30 | username: String,
31 | password: String) = {
32 |
33 | save(
34 | User(id = Sequence.nextId,
35 | user_id = user_id,
36 | username = username,
37 | password = HashUtils.md5(password),
38 | created_on = DateTime.now,
39 | deleted_on = None,
40 | deleted = false)
41 | )
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/utils/DAO.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.utils
2 |
3 | import scala.collection.mutable.HashSet
4 | import scala.collection.mutable.HashMap
5 |
6 | /**
7 | * Created by hasanozgan on 31/07/14.
8 | */
9 | class DAO[T <: Entity] {
10 | private val items = new HashSet[T]
11 |
12 | def insert(entity: T) = {
13 | items.add(entity)
14 | }
15 |
16 | def save(entity: T) = {
17 | items.add(entity)
18 | }
19 |
20 | def remove(entity: T) = {
21 | items.remove(entity)
22 | }
23 |
24 | def get(id: Long) = {
25 | items.find(p => p.getId == id)
26 | }
27 |
28 | def findBy(p: (T) => Boolean) = {
29 | items.find(p)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/utils/Entity.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.utils
2 |
3 | /**
4 | * Created by hasanozgan on 31/07/14.
5 | */
6 | class Entity(id: Long) {
7 | def getId = id
8 | }
9 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/adapters/inmemory/utils/Sequence.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.adapters.inmemory.utils
2 |
3 | import scala.util.Random
4 |
5 | /**
6 | * Created by hasanozgan on 31/07/14.
7 | */
8 | object Sequence {
9 | private val seq = new Random()
10 |
11 | def nextId: Long = seq.nextLong().abs
12 | }
13 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/authentication/ResourceAuthenticator.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.authentication
2 |
3 | import spray.oauth.{ OAuth2GrantHandler, OAuth2DataHandler, AuthInfo, AccessToken }
4 | import spray.routing.authentication.ContextAuthenticator
5 | import scala.concurrent.{ ExecutionContext, Future }
6 | import spray.routing.{ Rejection, RequestContext, AuthenticationFailedRejection }
7 | import spray.routing.AuthenticationFailedRejection.{ CredentialsMissing, CredentialsRejected }
8 | import spray.oauth.utils.OAuth2Utils._
9 | import spray.oauth.AuthInfo
10 | import scala.Some
11 | import spray.http.HttpHeaders.{ Authorization }
12 | import ExecutionContext.Implicits.global
13 |
14 | /**
15 | * Created with IntelliJ IDEA.
16 | * User: hasan.ozgan
17 | * Date: 6/12/14
18 | * Time: 12:21 PM
19 | * To change this template use File | Settings | File Templates.
20 | */
21 | trait ResourceAuthenticator {
22 |
23 | implicit def dataHandler: OAuth2DataHandler
24 |
25 | implicit def grantHandler: OAuth2GrantHandler
26 |
27 | def tokenAuthenticator: ContextAuthenticator[AuthInfo] = { ctx =>
28 | fetchAccessToken(ctx) match {
29 | case Some(token) =>
30 | dataHandler.findAuthInfoByAccessToken(token) match {
31 | case Some(info) => Future(Right(info))
32 | case None => Future(Left(AuthenticationFailedRejection(CredentialsRejected, List())))
33 | }
34 | case None => Future(Left(AuthenticationFailedRejection(CredentialsMissing, List())))
35 | }
36 | }
37 |
38 | def allowedScopes(info: AuthInfo, scopes: String*): Boolean = {
39 | !(toScopeList(info.scope) intersect scopes).isEmpty
40 | }
41 |
42 | /* Helpers */
43 |
44 | private def fetchAccessToken(ctx: RequestContext): Option[String] = {
45 | val found = ctx.request.header[`Authorization`]
46 | if (found.isDefined) {
47 | val REGEXP_AUTHORIZATION = """^\s*(OAuth|OAuth2|Bearer)\s+([^\s\,]*)""".r
48 | val matcher = REGEXP_AUTHORIZATION.findFirstMatchIn(found.get.value)
49 | if (matcher.isDefined && matcher.get.groupCount >= 2) Some(matcher.get.group(2)) else None
50 | } else ctx.request.uri.query.get("access_token")
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/authentication/SessionAuthenticator.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.authentication
2 |
3 | /**
4 | * Created with IntelliJ IDEA.
5 | * User: hasan.ozgan
6 | * Date: 6/12/14
7 | * Time: 12:21 PM
8 | * To change this template use File | Settings | File Templates.
9 | */
10 | trait SessionAuthenticator {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/directives/OAuth2Directives.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.directives
2 |
3 | import spray.routing._
4 | import spray.routing.directives._
5 | import shapeless.HNil
6 | import scala._
7 |
8 | import spray.http._
9 | import spray.http.HttpHeaders.{ Authorization, `X-Forwarded-For`, `Remote-Address` }
10 | import spray.oauth.utils.OAuth2Utils._
11 | import spray.oauth.utils.OAuth2Parameters._
12 |
13 | import spray.oauth._
14 | import spray.oauth.models._
15 | import spray.oauth.models.AuthResponse.AuthResponse
16 |
17 | import spray.oauth.models.AuthRequest
18 | import spray.oauth.models.AuthResponse.Approval
19 | import spray.routing.RequestContext
20 |
21 | import scala.Some
22 | import shapeless.::
23 | import spray.oauth.models.TokenRequest
24 | import spray.oauth.models.ResponseException
25 | import spray.routing.MissingCookieRejection
26 | import spray.oauth.AuthInfo
27 | import spray.oauth.models.TokenResponse.TokenResponse
28 | import spray.oauth.models.TokenResponse
29 | import scala.util.Try
30 | import spray.oauth.utils.{ TokenGenerator, OAuth2Utils, OAuth2Parameters }
31 |
32 | /**
33 | * Created with IntelliJ IDEA.
34 | * User: hasan.ozgan
35 | * Date: 4/15/14
36 | * Time: 9:57 AM
37 | * To change this template use File | Settings | File Templates.
38 | */
39 | trait OAuth2Directives extends FormFieldDirectives with ParameterDirectives {
40 |
41 | import BasicDirectives._
42 | import AnyParamDirectives._
43 | import HeaderDirectives._
44 | import MethodDirectives._
45 | import MarshallingDirectives._
46 |
47 | implicit def dataHandler: OAuth2DataHandler
48 |
49 | implicit def grantHandler: OAuth2GrantHandler
50 |
51 | /**
52 | *
53 | * @return
54 | */
55 | def fetchTokenRequest: Directive1[TokenRequest] =
56 | post & formFields('grant_type.as[GrantType.Value],
57 | 'client_id,
58 | 'client_secret,
59 | 'scope?,
60 | 'code?,
61 | 'redirect_uri?,
62 | 'refresh_token?,
63 | 'username?,
64 | 'password?).as(TokenRequest)
65 |
66 | /**
67 | *
68 | * @param request
69 | * @return
70 | */
71 | def grantHandler(request: TokenRequest): Directive1[TokenResponse] =
72 | extract { ctx =>
73 | dataHandler.getClient(request.client_id, request.client_secret) match {
74 | case None => TokenResponse.Error("invalid_client", Some(s"invalid client_id '${request.client_id}'"))
75 | case Some(client) if !client.hasGrant(request.grant_type) => TokenResponse.Error("invalid_grant", Some(s"invalid grant_type '${request.grant_type}' for this consumer"))
76 | case Some(client) if request.scopeRequired && !client.hasScope(request.getScopeList) => {
77 | val unknownScopes = request.getScopeList diff client.scopes
78 | TokenResponse.Error("invalid_scope", Some(s"invalid scopes '${unknownScopes.mkString(",")}' for this consumer"))
79 | }
80 | case Some(client) =>
81 | try {
82 | request.grant_type match {
83 | case GrantType.Password => grantHandler.password(request)
84 | case GrantType.ClientCredentials => grantHandler.clientCredentials(request)
85 | case GrantType.RefreshToken => grantHandler.refreshToken(request)
86 | case GrantType.AuthorizationCode => grantHandler.authorizationCode(request)
87 | }
88 | } catch {
89 | case e: ResponseException => TokenResponse.Error(e.error, e.error_description)
90 | }
91 | }
92 | }
93 |
94 | def approveForm: Directive1[List[String]] =
95 | entity(as[FormData]) map {
96 | case formData: FormData => extractApprovedScopes(formData) :: HNil
97 | }
98 |
99 | def authRequest(user: AuthUser, scopes: String): Directive1[AuthRequest] =
100 | parameters('response_type.as[ResponseType.Value],
101 | 'client_id.as[String],
102 | 'redirect_uri.as[String],
103 | 'scope.as[String] ? "",
104 | 'state.as[String] ? "",
105 | 'consumer_granted_scopes.as[String] ? "false",
106 | 'display.as[DisplayType.Value] ? DisplayType.PAGE,
107 | 'access_type.as[AccessType.Value] ? AccessType.ONLINE,
108 | 'approval_prompt.as[ApprovalPrompt.Value] ? ApprovalPrompt.AUTO,
109 | 'approved_scopes.as[String] ? scopes,
110 | 'user_id.as[AuthUser] ? user.id).as(AuthRequest)
111 |
112 | def fetchAuthRequest(user: AuthUser): Directive1[AuthRequest] =
113 | approveForm hflatMap {
114 | case scopes :: HNil =>
115 | authRequest(user, scopes.asInstanceOf[List[String]].mkString(" "))
116 | }
117 |
118 | /**
119 | *
120 | * @param request
121 | * @return
122 | */
123 | def authHandler(request: AuthRequest): Directive1[AuthResponse] =
124 | extract { ctx =>
125 | try {
126 | dataHandler.getClient(request.client_id) match {
127 | case None => AuthResponse.Error("invalid_client", None)
128 | case Some(client) if !client.hasGrant(request.getGrantType) => AuthResponse.Error("invalid_grant", Some(s"invalid grant_type '${request.getGrantType}' for this consumer"))
129 | case Some(client) if !client.hasScope(request.getScopeList) => {
130 | val unknownScopes = request.getScopeList diff client.scopes
131 | throw new ResponseException(s"invalid scopes '${unknownScopes.mkString(",")}' for this consumer")
132 | }
133 | case Some(client) => {
134 |
135 | val userScopesInfo = dataHandler.findAuthInfoByUser(request.client_id, request.user, request.getGrantType) match {
136 | case Some(info) => (info.scope, OAuth2Utils.toScopeList(info.scope))
137 | case None => (None, List.empty)
138 | }
139 |
140 | val scopeMap = generateScopeMap(request.getScopeList, userScopesInfo._2)
141 | val requiredScopes = scopeMap.filter(x => !x._2)
142 |
143 | if (isGetMethod(ctx) && (request.approvalPromptForced || OAuth2Utils.isNotSameScopes(userScopesInfo._1, Some(request.scope)) || requiredScopes.size > 0)) {
144 | Approval(ApprovalForm(request.client_id, scopeMap))
145 | } else if (isPostMethod(ctx) && !request.hasApprovedScopes) {
146 | AuthResponse.Error("canceled_by_user", Some("canceled by user"))
147 | } else {
148 | val approvedScopes = if (request.approved_scopes.isEmpty) request.scope else request.approved_scopes
149 | val authInfo = AuthInfo(request.optionalUser, Some(request.client_id), Some(approvedScopes), Some(request.redirect_uri), request.isRefreshable, request.getGrantType, request.getClientIP(ctx))
150 |
151 | val tokenResponse =
152 | request.getGrantType match {
153 | case GrantType.Implicit => grantHandler.implicitToken(authInfo)
154 | case GrantType.AuthorizationCode => grantHandler.authorizationCode(authInfo)
155 | }
156 |
157 | AuthResponse.Redirect(generateUri(request.redirect_uri, request.state, tokenResponse))
158 | }
159 | }
160 | }
161 | } catch {
162 | case ex: ResponseException => AuthResponse.Error(ex.error, ex.error_description)
163 | case e: Exception => {
164 | e.printStackTrace()
165 | AuthResponse.Error("BadRequest", Some(e.getMessage))
166 | }
167 | }
168 | }
169 |
170 | /* Helpers */
171 | private def isGetMethod(ctx: RequestContext) = ctx.request.method == HttpMethods.GET
172 |
173 | private def isPostMethod(ctx: RequestContext) = ctx.request.method == HttpMethods.POST
174 |
175 | private def generateScopeMap(requestScopes: List[String], userScopes: List[String]): Map[String, Boolean] = {
176 | val requestedScopeMap = requestScopes map { t => (t, false) } toMap
177 | val userScopeMap = userScopes map { t => (t, true) } toMap
178 |
179 | mergeMap(requestedScopeMap, userScopeMap)
180 | }
181 |
182 | private def mergeMap[K, V](ts: Map[K, V], xs: Map[K, V]): Map[K, V] =
183 | (ts /: xs) {
184 | case (acc, entry) =>
185 | if (entry._2 == true) acc + entry else acc
186 | }
187 |
188 | private def generateUri(redirectUri: String, state: String, response: TokenResponse): Uri = {
189 | val r = response match {
190 | case token: TokenResponse.Token => {
191 | val scopeParam = token.scope.fold("")(x => s"&scope=${x}")
192 | s"#access_token=${token.access_token}&token_type=${token.token_type}&expires_in=${token.expires_in}${scopeParam}"
193 | }
194 | case code: TokenResponse.Code => s"?code=${code.code}"
195 | case err: TokenResponse.Error => s"?error=${err.error}&error_description=${err.error_description.getOrElse("N/A")}"
196 | }
197 |
198 | redirectUri + r
199 | }
200 |
201 | private def extractApprovedScopes(f: FormData): List[String] = {
202 | f.fields.toMap
203 | .filter(x => x._1.startsWith(APPROVAL_FORM_PREFIX_FOR_SCOPE_KEY) && Try(x._2.toBoolean).getOrElse(false))
204 | .map(x => x._1.stripPrefix(APPROVAL_FORM_PREFIX_FOR_SCOPE_KEY))
205 | .toList
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/directives/ResourceDirectives.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.directives
2 |
3 | import spray.routing._
4 | import spray.routing.directives.BasicDirectives._
5 | import scala.Some
6 | import spray.oauth.utils.OAuth2Parameters
7 |
8 | /**
9 | * Created with IntelliJ IDEA.
10 | * User: hasan.ozgan
11 | * Date: 3/3/14
12 | * Time: 12:52 PM
13 | * To change this template use File | Settings | File Templates.
14 | */
15 |
16 | //http://kufli.blogspot.com/2013/08/sprayio-rest-services-api-versioning.html
17 | trait ResourceDirectives {
18 |
19 | def contentTypeVersion(contentType: String): Directive1[String] =
20 | extract { ctx =>
21 | val header = ctx.request.headers.find(_.name == "Content-Type")
22 | header match {
23 | //TODO Parse content-type value with 'contentType' parameter
24 | case Some(head) => head.value
25 | case _ => "1" //default to 1
26 | }
27 | }
28 |
29 | def headerVersion: Directive1[String] =
30 | extract { ctx =>
31 | val header = ctx.request.headers.find(_.name == OAuth2Parameters.RESOURCE_HEADERS_VERSIONING)
32 | header match {
33 | case Some(head) => head.value
34 | case _ => "1" //default to 1
35 | }
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/directives/SessionDirectives.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.directives
2 |
3 | import spray.routing.{ AuthenticationFailedRejection, Directive1, Directive0, StandardRoute }
4 | import spray.http.{ HttpHeaders, DateTime, HttpCookie }
5 | import spray.routing.directives._
6 | import spray.routing._
7 | import spray.httpx.marshalling.ToResponseMarshallable
8 | import scala.concurrent.Future
9 | import spray.routing.authentication._
10 | import spray.routing.RequestContext
11 | import spray.routing.directives.CookieDirectives._
12 | import spray.routing.MissingCookieRejection
13 | import scala.Some
14 |
15 | import spray.util._
16 | import spray.http._
17 | import HttpHeaders._
18 | import spray.routing.MissingCookieRejection
19 | import scala.Some
20 | import spray.routing
21 | import shapeless.HNil
22 | import spray.routing.AuthenticationFailedRejection.CredentialsRejected
23 | import spray.oauth.utils.{ OAuth2Parameters, TokenGenerator }
24 | import spray.oauth.rejections.SecurePageRejection
25 |
26 | /**
27 | * Created by hasanozgan on 20/03/14.
28 | */
29 | trait SessionDirectives {
30 | import BasicDirectives._
31 | import CookieDirectives._
32 | import RouteDirectives._
33 | import HeaderDirectives._
34 | import RespondWithDirectives._
35 |
36 | def session: Directive1[HttpCookie] =
37 | extract {
38 | ctx =>
39 | {
40 | val sid = findSessionCookie(ctx)
41 |
42 | if (!sid.isDefined)
43 | ctx.reject(SecurePageRejection(ctx.request.uri))
44 |
45 | sid.get
46 | }
47 | }
48 |
49 | def deleteSession: Directive0 = {
50 | deleteCookie(name = OAuth2Parameters.APPLICATION_SESSION_KEY, path = "/")
51 | }
52 |
53 | def createSession: Directive0 = {
54 | val token = TokenGenerator.bearer
55 | val cookie = HttpCookie(name = OAuth2Parameters.APPLICATION_SESSION_KEY, domain = OAuth2Parameters.APPLICATION_SESSION_COOKIE_DOMAIN, content = token, path = Some("/"), secure = true)
56 | setCookie(cookie)
57 | }
58 |
59 | private def findSessionCookie(ctx: RequestContext) = {
60 | ctx.request.cookies.find(_.name equals OAuth2Parameters.APPLICATION_SESSION_KEY)
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/endpoints/AuthorizeService.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.endpoints
2 |
3 | import spray.routing.{ RequestContext, HttpService }
4 | import spray.oauth.directives.OAuth2Directives
5 | import spray.http.{ HttpCredentials, HttpHeader, HttpRequest, StatusCodes }
6 | import spray.oauth.models.AuthResponse.{ Error, Redirect, Approval }
7 | import spray.routing.authentication._
8 | import scala.concurrent.{ ExecutionContext, Future }
9 | import ExecutionContext.Implicits.global
10 | import spray.routing.authentication.UserPass
11 | import spray.oauth.models.{ ApprovalForm, AuthResponse, AuthRequest }
12 | import spray.oauth.{ AuthUser, AuthInfo }
13 | import spray.oauth.utils.OAuth2Parameters._
14 | import spray.oauth.utils.OAuth2Utils
15 | import spray.http._
16 | import MediaTypes._
17 |
18 | /**
19 | * Created by hasanozgan on 03/06/14.
20 | */
21 | trait AuthorizeService extends HttpService with OAuth2Directives {
22 |
23 | case class User(id: String, name: String)
24 |
25 | def myUserPassAuthenticator(userPass: Option[UserPass]): Future[Option[AuthUser]] = {
26 | Future {
27 | if (userPass.isDefined)
28 | dataHandler.getUser(userPass.get.user, userPass.get.pass)
29 | else None
30 | //dataHandler.findUser(userPass.get.g
31 | /**
32 | * if (userPass.exists(up => up.user == "user" && up.pass == "pass")) dataHandler.findUser(user)
33 | * else None
34 | */
35 | }
36 | }
37 |
38 | val defaultAuthorizeRoutes =
39 | path("authorize") {
40 | authenticate(BasicAuth(myUserPassAuthenticator _, realm = "secure site")) { user =>
41 | fetchAuthRequest(user) { request =>
42 | //protectRequest(request, user) { csrf_token =>
43 | authHandler(request) {
44 | case AuthResponse.Approval(form) => respondWithMediaType(`text/html`) {
45 | complete(renderApprovalForm(form))
46 | }
47 | case AuthResponse.Redirect(uri) => redirect(uri, StatusCodes.TemporaryRedirect)
48 | case AuthResponse.Error(err, desc) => complete(s"ERROR: ${err}") //render("tpl/approvalPage", request)
49 | }
50 | // }
51 | }
52 | }
53 | } ~
54 | path("login") {
55 | get {
56 | complete("OK")
57 | }
58 | }
59 |
60 | private def renderApprovalForm(form: ApprovalForm) = {
61 |
62 | s"""
63 |
64 | OAuth Approval
65 | Do you authorize '${form.client_id}' to access your protected resources?
66 |
71 |
72 |
77 |
78 | """
79 | }
80 |
81 | private def renderScopes(form: ApprovalForm) = {
82 | form.scopes.map(scope =>
83 | s"""
84 |
85 | ${scope._1}:
86 |
87 |
88 |
89 | """).mkString("")
90 | }
91 |
92 | def scopeSelected(inputType: Boolean, scopeStatus: Boolean) = {
93 | if (inputType == scopeStatus) "checked=checked" else ""
94 | }
95 |
96 | }
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/endpoints/OAuth2Services.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.endpoints
2 |
3 | /**
4 | * Created by hasanozgan on 06/06/14.
5 | */
6 | trait OAuth2Services extends TokenService with AuthorizeService {
7 |
8 | val defaultOAuth2Routes = defaultTokenRoutes ~ defaultAuthorizeRoutes
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/endpoints/TokenService.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.endpoints
2 |
3 | import spray.json.DefaultJsonProtocol
4 |
5 | import spray.routing.HttpService
6 | import spray.oauth.directives.OAuth2Directives
7 | import spray.oauth.models.TokenRequest
8 | import spray.http.StatusCodes
9 | import spray.oauth.models.TokenResponse.{ Token, Error, Code }
10 | import spray.httpx.SprayJsonSupport
11 |
12 | object MyJsonProtocol extends DefaultJsonProtocol {
13 | implicit val TokenFormat = jsonFormat5(Token)
14 | implicit val CodeFormat = jsonFormat1(Code)
15 | implicit val ErrorFormat = jsonFormat2(Error)
16 | }
17 |
18 | import MyJsonProtocol._
19 |
20 | /**
21 | * Created by hasanozgan on 03/06/14.
22 | */
23 | trait TokenService extends HttpService with SprayJsonSupport with OAuth2Directives {
24 |
25 | val defaultTokenRoutes =
26 |
27 | path("token") {
28 |
29 | fetchTokenRequest { request: TokenRequest =>
30 | grantHandler(request) {
31 | case error: Error => complete(error)
32 | case token: Token => complete(token)
33 | case code: Code => complete(code)
34 | }
35 | }
36 | } ~
37 | path("revoke") {
38 | get {
39 | complete {
40 | "TODO"
41 | }
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/models/AuthModel.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.models
2 |
3 | import spray.http.Uri
4 | import spray.oauth.{ AuthUser, AuthInfo, AccessToken }
5 | import spray.oauth.utils.{ OAuth2Utils, OAuth2Parameters }
6 | import OAuth2Parameters._
7 | import spray.oauth.utils.OAuth2Utils._
8 | import scala.Some
9 | import spray.routing.RequestContext
10 |
11 | /**
12 | * Created with IntelliJ IDEA.
13 | * User: hasan.ozgan
14 | * Date: 4/15/14
15 | * Time: 2:13 PM
16 | * To change this template use File | Settings | File Templates.
17 | */
18 |
19 | /**
20 | * Created with IntelliJ IDEA.
21 | * User: hasan.ozgan
22 | * Date: 4/15/14
23 | * Time: 2:10 PM
24 | * To change this template use File | Settings | File Templates.
25 | */
26 |
27 | case class AuthRequest(response_type: ResponseType.Value,
28 | client_id: String,
29 | redirect_uri: String,
30 | scope: String,
31 | state: String = "",
32 | consumer_granted_scopes: String = "false",
33 | display: DisplayType.Value,
34 | access_type: AccessType.Value,
35 | approval_prompt: ApprovalPrompt.Value,
36 | approved_scopes: String = "",
37 | user_id: String = "") {
38 | require(!client_id.isEmpty, "client id must not be empty")
39 | require(consumerScopeEmptyRule, "scope must not be empty")
40 | require(DisplayType.values.exists(_.equals(display)), s"Invalid display: ${display}")
41 | require(ResponseType.values.exists(_.equals(response_type)), s"Invalid response_type: ${response_type}")
42 | require(AccessType.values.exists(_.equals(access_type)), s"Invalid access_type: ${access_type}")
43 | require(ApprovalPrompt.values.exists(_.equals(approval_prompt)), s"Invalid approval_prompt: ${approval_prompt}")
44 |
45 | private def consumerScopeEmptyRule: Boolean = {
46 | !(scope.isEmpty && !useConsumerGrantedScopes)
47 | }
48 |
49 | def optionalUser = {
50 | if (user_id.isEmpty) None else Some(user)
51 | }
52 |
53 | def user = AuthUser(user_id)
54 |
55 | def getClientIP(ctx: RequestContext): Option[String] = {
56 | // Just Only For Implicit Token
57 | if (getGrantType.equals(GrantType.Implicit)) OAuth2Utils.getClientIP(ctx) else None
58 | }
59 |
60 | def isRefreshable = {
61 | AccessType.OFFLINE == access_type
62 | }
63 |
64 | def getGrantType = {
65 | if (ResponseType.TOKEN == response_type) GrantType.Implicit else GrantType.AuthorizationCode
66 | }
67 |
68 | def approvalPromptForced = {
69 | ApprovalPrompt.FORCE == approval_prompt
70 | }
71 |
72 | def hasApprovedScopes = {
73 | !toScopeList(Some(approved_scopes)).isEmpty
74 | }
75 |
76 | def useConsumerGrantedScopes = {
77 | if (consumer_granted_scopes.isEmpty) false
78 | else {
79 | try {
80 | consumer_granted_scopes.toBoolean
81 | } catch {
82 | case e: Exception => false
83 | }
84 | }
85 | }
86 | def getScopeList = {
87 | OAuth2Utils.toScopeList(Some(scope))
88 | }
89 | }
90 |
91 | case class ApprovalForm(client_id: String, scopes: Map[String, Boolean])
92 |
93 | object AuthResponse {
94 | sealed trait AuthResponse
95 | case class Approval(form: ApprovalForm) extends AuthResponse
96 | case class Error(error: String, error_description: Option[String]) extends AuthResponse
97 | case class Redirect(uri: Uri) extends AuthResponse
98 | }
99 |
100 | object DisplayType extends Enumeration {
101 | val PAGE = Value("page")
102 | val TOUCH = Value("touch")
103 | val DIALOG = Value("dialog")
104 | val POPUP = Value("popup")
105 |
106 | implicit def convertFromString(value: String): DisplayType.Value = try { DisplayType.withName(value) } catch { case ex: Exception => throw new Exception(s"Invalid display: ${value}") }
107 | implicit def convertToString(value: DisplayType.Value): String = value.toString
108 | }
109 |
110 | object ResponseType extends Enumeration {
111 | val CODE = Value("code")
112 | val TOKEN = Value("token")
113 |
114 | implicit def convertFromString(value: String): ResponseType.Value = try { ResponseType.withName(value) } catch { case ex: Exception => throw new Exception(s"Invalid response_type: ${value}") }
115 | implicit def convertToString(value: ResponseType.Value): String = value.toString
116 | }
117 |
118 | object AccessType extends Enumeration {
119 | val ONLINE = Value("online")
120 | val OFFLINE = Value("offline")
121 |
122 | implicit def convertFromString(value: String): AccessType.Value = try { AccessType.withName(value) } catch { case ex: Exception => AccessType.ONLINE }
123 | implicit def convertToString(value: AccessType.Value): String = value.toString
124 | }
125 |
126 | object ApprovalPrompt extends Enumeration {
127 | val AUTO = Value("auto")
128 | val FORCE = Value("force")
129 |
130 | implicit def convertFromString(value: String): ApprovalPrompt.Value = try { ApprovalPrompt.withName(value) } catch { case ex: Exception => ApprovalPrompt.AUTO }
131 | implicit def convertToString(value: ApprovalPrompt.Value): String = value.toString
132 | }
133 |
134 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/models/TokenModel.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.models
2 |
3 | import spray.oauth.utils.{ OAuth2Utils, OAuth2Parameters }
4 |
5 | /**
6 | * Created with IntelliJ IDEA.
7 | * User: hasan.ozgan
8 | * Date: 3/6/14
9 | * Time: 9:34 AM
10 | * To change this template use File | Settings | File Templates.
11 | */
12 |
13 | case class TokenRequest(grant_type: GrantType.Value,
14 | client_id: String,
15 | client_secret: String,
16 | scope: Option[String],
17 | code: Option[String],
18 | redirect_uri: Option[String],
19 | refresh_token: Option[String],
20 | username: Option[String],
21 | password: Option[String]) {
22 |
23 | def scopeRequired = {
24 | grant_type match {
25 | case GrantType.Password => true
26 | case GrantType.ClientCredentials => true
27 | case _ => false
28 | }
29 | }
30 |
31 | require(!grant_type.isEmpty, "grant_type must not be empty")
32 | require(GrantType.values.exists(_.equals(grant_type)), s"Invalid grant_type: ${grant_type}")
33 | require(required(GrantType.AuthorizationCode, code), "code required")
34 |
35 | require(required(GrantType.RefreshToken, refresh_token), "refresh_token required")
36 |
37 | require(required(GrantType.Password, scope), "scope required")
38 | require(required(GrantType.Password, username), "username required")
39 | require(required(GrantType.Password, password), "password required")
40 |
41 | require(required(GrantType.ClientCredentials, scope), "scope required")
42 |
43 | require(notEmpty(GrantType.AuthorizationCode, code), "code must not be empty")
44 |
45 | require(notEmpty(GrantType.RefreshToken, refresh_token), "refresh_token must not be empty")
46 |
47 | require(notEmpty(GrantType.Password, username), "username must not be empty")
48 | require(notEmpty(GrantType.Password, password), "password must not be empty")
49 |
50 | def notEmpty(grantType: GrantType.Value, member: Option[String]) = {
51 | if (grant_type.equals(grantType)) !member.getOrElse("").isEmpty else true
52 | }
53 |
54 | def getScopeList = {
55 | OAuth2Utils.toScopeList(scope)
56 | }
57 |
58 | def required(grantType: GrantType.Value, member: Option[String]) = {
59 | if (grant_type.equals(grantType)) !member.isEmpty else true
60 | }
61 | }
62 |
63 | object TokenResponse {
64 | sealed trait TokenResponse
65 | case class Token(token_type: String, access_token: String, refresh_token: Option[String], scope: Option[String], expires_in: Long) extends TokenResponse
66 | case class Code(code: String) extends TokenResponse
67 | case class Error(error: String, error_description: Option[String]) extends TokenResponse
68 | }
69 |
70 | case class ResponseException(error: String, error_description: Option[String] = None) extends Exception
71 |
72 | object GrantType extends Enumeration {
73 | val AuthorizationCode = Value("authorization_code")
74 | val RefreshToken = Value("refresh_token")
75 | val ClientCredentials = Value("client_credentials")
76 | val Password = Value("password")
77 | val Implicit = Value("implicit")
78 |
79 | implicit def convertFromString(value: String): GrantType.Value = try { GrantType.withName(value) } catch { case ex: Exception => throw new Exception(s"invalid grant_type: ${value}") }
80 | implicit def convertToString(value: GrantType.Value): String = value.toString
81 | }
82 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/rejections/AuthHandlerErrorRejection.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.rejections
2 |
3 | import spray.routing.Rejection
4 |
5 | /**
6 | * Created by hasanozgan on 16/05/14.
7 | */
8 | case class AuthHandlerErrorRejection(error: String) extends Rejection
9 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/rejections/SecurePageRejection.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.rejections
2 |
3 | import spray.routing.Rejection
4 | import spray.http.Uri
5 |
6 | /**
7 | * Created with IntelliJ IDEA.
8 | * User: hasan.ozgan
9 | * Date: 4/7/14
10 | * Time: 5:11 PM
11 | * To change this template use File | Settings | File Templates.
12 | */
13 | case class SecurePageRejection(uri: Uri) extends Rejection
14 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/utils/CSRFUtils.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.utils
2 |
3 | import spray.http.HttpMethods
4 | import spray.oauth.models.AuthRequest
5 | import spray.oauth.utils.OAuth2Parameters._
6 | import spray.routing.{ MalformedHeaderRejection, MissingHeaderRejection, RequestContext }
7 |
8 | /**
9 | * Created by hasan.ozgan on 7/7/2014.
10 | * TODO...
11 | */
12 | class CSRFUtils[U] {
13 | def generateCsrfToken(request: AuthRequest, user: U): Option[String] = {
14 | if (!APPROVAL_FORM_CSRF_ENABLED) None
15 | else {
16 | val crcKey = TokenGenerator.hash(8)
17 | val hash = HashUtils.md5("%s|%s|%s|%s|%s|%s".format(request.client_id, user, request.scope, request.getGrantType, crcKey, OAuth2Parameters.SECRET))
18 | Some(crcKey + hash.substring(8))
19 | }
20 | }
21 |
22 | def confirmCsrfToken(csrf: String, request: AuthRequest, user: U): Boolean = {
23 | if (!APPROVAL_FORM_CSRF_ENABLED) true
24 | else if (csrf.isEmpty || csrf.length < 8) false
25 | else {
26 | val crcKey = csrf.substring(0, 8)
27 | val hash = HashUtils.md5("%s|%s|%s|%s|%s|%s".format(request.client_id, user, request.scope, request.getGrantType, crcKey, OAuth2Parameters.SECRET))
28 |
29 | csrf.substring(8).equals(hash.substring(8))
30 | }
31 | true
32 | }
33 |
34 | def checkCsrfToken(request: AuthRequest, ctx: RequestContext, user: U): Boolean = {
35 | var status = true
36 | val csrfList = ctx.request.headers.filter(x => x.is("X-XSRF-TOKEN")).map(x => x.value.toString)
37 |
38 | if (APPROVAL_FORM_CSRF_ENABLED && isPostMethod(ctx)) {
39 | if (csrfList.isEmpty) {
40 | status = false
41 | ctx.reject(MissingHeaderRejection("X-XSRF-TOKEN"))
42 | } else if (confirmCsrfToken(csrfList.head, request, user)) {
43 | status = false
44 | ctx.reject(MalformedHeaderRejection("X-XSRF-TOKEN", "token is invalid"))
45 | }
46 | }
47 |
48 | status
49 | }
50 |
51 | private def isPostMethod(ctx: RequestContext) = ctx.request.method == HttpMethods.POST
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/utils/DefaultGrantHandler.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.utils
2 |
3 | import spray.oauth._
4 | import spray.oauth.models._
5 | import spray.oauth.AccessToken
6 | import spray.oauth.models.TokenResponse.{ Code, Token, TokenResponse }
7 | import scala.Some
8 | import spray.oauth.utils.OAuth2Parameters._
9 | import spray.oauth.models.TokenResponse.TokenResponse
10 | import spray.oauth.models.TokenResponse
11 | import spray.oauth.models.ResponseException
12 | import spray.oauth.AccessToken
13 | import spray.oauth.models.TokenResponse.Code
14 | import spray.oauth.AuthInfo
15 | import spray.oauth.models.TokenResponse.Token
16 | import scala.Some
17 | import spray.oauth.models.TokenRequest
18 |
19 | /**
20 | * Created with IntelliJ IDEA.
21 | * User: hasan.ozgan
22 | * Date: 4/16/14
23 | * Time: 4:15 PM
24 | * To change this template use File | Settings | File Templates.
25 | */
26 | object DefaultGrantHandler extends OAuth2GrantHandler {
27 |
28 | override def authorizationCode(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
29 | dataHandler.findAuthInfoByCode(request.code.get) match {
30 | case Some(authInfo) if authInfo.clientId.exists(x => x.equals(request.client_id)) => {
31 | dataHandler.deleteCode(request.code.get)
32 | issueAccessToken(authInfo)
33 | }
34 | case _ => TokenResponse.Error("invalid_grant", None)
35 | }
36 | }
37 |
38 | override def authorizationCode(authInfo: AuthInfo)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
39 | issueAuthorizationCode(authInfo)
40 | }
41 |
42 | override def clientCredentials(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
43 | val authInfo = AuthInfo(None, Some(request.client_id), request.scope, None, refreshable = true, GrantType.ClientCredentials, None)
44 |
45 | issueAccessToken(authInfo)
46 | }
47 |
48 | override def password(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
49 | val client = dataHandler.getClient(request.client_id).getOrElse(throw new ResponseException("invalid_client"))
50 | val user = dataHandler.getUser(request.username.get, request.password.get).getOrElse(throw new ResponseException("InvalidGrant"))
51 | val authInfo = AuthInfo(Some(user), Some(request.client_id), request.scope, None, refreshable = client.hasGrant(GrantType.RefreshToken), GrantType.Password, None)
52 |
53 | issueAccessToken(authInfo, showRefreshToken = true)
54 | }
55 |
56 | override def refreshToken(request: TokenRequest)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
57 | val refreshToken = request.refresh_token.getOrElse("")
58 | val authInfo = dataHandler.findAuthInfoByRefreshToken(refreshToken).getOrElse(throw new ResponseException("InvalidToken"))
59 |
60 | issueAccessToken(authInfo, showRefreshToken = false)
61 | }
62 |
63 | override def implicitToken(authInfo: AuthInfo)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
64 | issueAccessToken(authInfo, showRefreshToken = false)
65 | }
66 |
67 | /* Helper Methods */
68 |
69 | private def issueAuthorizationCode(authInfo: AuthInfo)(implicit dataHandler: OAuth2DataHandler): TokenResponse.TokenResponse = {
70 | dataHandler.createCode(authInfo) match {
71 | case Some(code) => TokenResponse.Code(code)
72 | case _ => TokenResponse.Error("internal_error", None)
73 | }
74 | }
75 |
76 | private def issueAccessToken(authInfo: AuthInfo, showRefreshToken: Boolean = true)(implicit dataHandler: OAuth2DataHandler): TokenResponse = {
77 | val accessToken = dataHandler.findAccessToken(authInfo) match {
78 | case Some(token) if dataHandler.isAccessTokenExpired(token) || OAuth2Utils.isNotSameScopes(token.scope, authInfo.scope) =>
79 | {
80 | token.refreshToken match {
81 | case Some(rt) => dataHandler.refreshAccessToken(authInfo, rt).getOrElse(throw new Exception("refresh_token is not generated"))
82 | case None => dataHandler.createAccessToken(authInfo).getOrElse(throw new Exception("token is not generated"))
83 | }
84 | }
85 | case Some(token) => token
86 | case _ => dataHandler.createAccessToken(authInfo).getOrElse(throw new Exception("token is not generated"))
87 | }
88 |
89 | toTokenResponse(accessToken, authInfo.refreshable && showRefreshToken)
90 | }
91 |
92 | private def toTokenResponse(accessToken: AccessToken, showRefreshToken: Boolean = true): TokenResponse = {
93 | Token(
94 | accessToken.tokenType,
95 | accessToken.token,
96 | if (showRefreshToken) accessToken.refreshToken else None,
97 | if (SHOW_SCOPE) accessToken.scope else None,
98 | accessToken.expiresIn
99 | )
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/utils/HashUtils.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.utils
2 |
3 | import java.security.MessageDigest
4 |
5 | /**
6 | * Created by hasanozgan on 07/07/14.
7 | */
8 | object HashUtils {
9 | def md5(s: String) = {
10 | val md5 = MessageDigest.getInstance("MD5")
11 | md5.reset()
12 | md5.update(s.getBytes("UTF-8"))
13 | md5.digest().map(0xFF & _).map { "%02x".format(_) }.foldLeft("") { _ + _ }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/utils/OAuth2Parameters.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.utils
2 |
3 | import com.typesafe.config.ConfigFactory
4 |
5 | /**
6 | * Created with IntelliJ IDEA.
7 | * User: hasan.ozgan
8 | * Date: 4/18/14
9 | * Time: 10:40 AM
10 | * To change this template use File | Settings | File Templates.
11 | */
12 | object OAuth2Parameters {
13 | lazy val conf = ConfigFactory.load()
14 |
15 | val SCOPE_SEPARATOR = conf.getString("spray.oauth2.scope-separator")
16 | val TOKEN_DURATION = conf.getMilliseconds("spray.oauth2.token-duration")
17 | val TOKEN_LENGTH = conf.getInt("spray.oauth2.token-length")
18 | val CODE_DURATION = conf.getMilliseconds("spray.oauth2.code-duration")
19 | val CODE_LENGTH = conf.getInt("spray.oauth2.code-length")
20 | val REFRESH_TOKEN_LENGTH = conf.getInt("spray.oauth2.refresh-token-length")
21 | val CONSUMER_SECRET_LENGTH = conf.getInt("spray.oauth2.consumer-secret-length")
22 | val SHOW_SCOPE = conf.getBoolean("spray.oauth2.show-scope")
23 | val SECRET = conf.getString("spray.oauth2.secret")
24 |
25 | val APPLICATION_SESSION_KEY = conf.getString("spray.oauth2.application.session-key")
26 | val APPLICATION_SESSION_COOKIE_DOMAIN = getCookieDomain
27 |
28 | val RESOURCE_HEADERS_VERSIONING = conf.getString("spray.oauth2.resource.headers.versioning")
29 |
30 | val APPROVAL_FORM_CSRF_ENABLED = conf.getBoolean("spray.oauth2.approval-form.csrf-enabled")
31 | val APPROVAL_FORM_CSRF_TOKEN_KEY = conf.getString("spray.oauth2.approval-form.csrf-token-key")
32 | val APPROVAL_FORM_PREFIX_FOR_SCOPE_KEY = conf.getString("spray.oauth2.approval-form.prefix-for-scope-key")
33 |
34 | private def getCookieDomain = {
35 | val domain = conf.getString("spray.oauth2.application.session-cookie-domain")
36 | if (domain.isEmpty) None else Some(domain)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/utils/OAuth2Utils.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.utils
2 |
3 | import spray.oauth.utils.OAuth2Parameters._
4 | import spray.routing.RequestContext
5 | import spray.http.HttpHeader
6 | import spray.http.HttpHeaders.{ `Remote-Address`, `X-Forwarded-For` }
7 | import java.security.MessageDigest
8 |
9 | /**
10 | * Created with IntelliJ IDEA.
11 | * User: hasan.ozgan
12 | * Date: 4/18/14
13 | * Time: 12:49 PM
14 | * To change this template use File | Settings | File Templates.
15 | */
16 | object OAuth2Utils {
17 |
18 | def isSameScopes(requestedScopes: Option[String], authorizedScopes: Option[String]): Boolean = {
19 | val tokenScopeList = OAuth2Utils.toScopeList(requestedScopes)
20 | val authInfoScopeList = OAuth2Utils.toScopeList(authorizedScopes)
21 |
22 | tokenScopeList sameElements authInfoScopeList
23 | }
24 |
25 | def isNotSameScopes(requestedScopes: Option[String], authorizedScopes: Option[String]): Boolean = {
26 | !isSameScopes(requestedScopes, authorizedScopes)
27 | }
28 |
29 | def toScopeList(scope: Option[String]): List[String] = {
30 | scope.getOrElse("").split(SCOPE_SEPARATOR).distinct.toList.filter(x => !x.isEmpty)
31 | }
32 |
33 | def getClientIP(ctx: RequestContext): Option[String] = {
34 | val found: Option[HttpHeader] =
35 | ctx.request.headers.find {
36 | case `X-Forwarded-For`(Seq(address, _*)) => true
37 | case `Remote-Address`(address) => true
38 | case h if h.is("x-real-ip") => true
39 | }
40 |
41 | if (found.isDefined) Some(found.get.value) else None
42 | }
43 |
44 | def md5(s: String) = {
45 | MessageDigest.getInstance("MD5").digest(s.getBytes)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/core/src/main/scala/spray/oauth/utils/TokenGenerator.scala:
--------------------------------------------------------------------------------
1 | package spray.oauth.utils
2 |
3 | import java.security.SecureRandom
4 | import spray.oauth.utils.OAuth2Parameters._
5 |
6 | /**
7 | * Created with IntelliJ IDEA.
8 | * User: hasan.ozgan
9 | * Date: 3/25/14
10 | * Time: 12:12 PM
11 | * To change this template use File | Settings | File Templates.
12 | */
13 | object TokenGenerator {
14 | val TOKEN_HASH_CHARS = "0123456789abcdef"
15 | val TOKEN_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._"
16 | val secureRandom = new SecureRandom()
17 |
18 | def hash(length: Int): String =
19 | if (length == 0) ""
20 | else TOKEN_HASH_CHARS(secureRandom.nextInt(TOKEN_HASH_CHARS.length())) + hash(length - 1)
21 |
22 | def bearer: String =
23 | bearer(TOKEN_LENGTH)
24 |
25 | def bearer(tokenLength: Int): String =
26 | if (tokenLength == 0) ""
27 | else TOKEN_CHARS(secureRandom.nextInt(TOKEN_CHARS.length())) + bearer(tokenLength - 1)
28 | }
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.1
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "spray repo" at "http://repo.spray.io"
2 |
3 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.1")
4 |
5 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.2.1")
6 |
7 | addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
8 |
9 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.3.2")
10 |
11 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.10.2")
12 |
13 | addSbtPlugin("com.orrsella" % "sbt-sublime" % "1.0.9")
14 |
15 | addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
16 |
17 |
--------------------------------------------------------------------------------
/samples/inmemory-webapp/build.sbt:
--------------------------------------------------------------------------------
1 | name := "spray-oauth2-demo"
2 |
3 | version := "0.1"
4 |
5 | unmanagedResourceDirectories in Compile <++= baseDirectory { base =>
6 | Seq( base / "src/main/webapp" )
7 | }
8 |
9 | resolvers ++= Seq(
10 | "Spray repo" at "http://repo.spray.io/",
11 | "Typesafe repo" at "http://repo.typesafe.com/typesafe/releases"
12 | )
13 |
14 | libraryDependencies ++= Seq(
15 | "io.spray" %% "spray-can" % "1.3.1",
16 | "io.spray" %% "spray-routing" % "1.3.1",
17 | "io.spray" %% "spray-httpx" % "1.3.0",
18 | "io.spray" %% "spray-json" % "1.3.1",
19 | "com.typesafe.akka" %% "akka-actor" % "2.3.5",
20 | "io.spray" %% "spray-testkit" % "1.3.1" % "test",
21 | "com.typesafe.akka" %% "akka-testkit" % "2.3.5" % "test"
22 | )
23 |
24 | seq(Revolver.settings: _*)
25 |
26 | seq(Twirl.settings: _*)
27 |
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | akka {
2 | loglevel = INFO
3 | }
4 |
5 | spray.can {
6 | # ref http://spray.io/documentation/1.1-M8/spray-can/configuration/
7 | server {
8 | request-timeout = 10s
9 |
10 | remote-address-header = on
11 | }
12 | }
13 |
14 | spray.oauth2 {
15 | scope-separator = " "
16 | token-duration = 1h
17 | token-length = 32
18 | consumer-secret-length = 32
19 | show-scope = true
20 |
21 | application {
22 | session-key = "SessionID"
23 | }
24 |
25 | approval-form {
26 | csrf-enabled = false
27 | }
28 |
29 | datasource {
30 | uri = "mongodb://localhost:27017/oauth2"
31 | }
32 | }
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/scala/com/hasanozgan/demo/inmemory/Boot.scala:
--------------------------------------------------------------------------------
1 | package com.hasanozgan.demo.inmemory
2 |
3 | import akka.actor.{ ActorSystem, Props }
4 | import spray.routing.SimpleRoutingApp
5 | import org.netology.spray.rest.oauth2.OAuth2Actor
6 |
7 | object Boot extends App with SimpleRoutingApp {
8 |
9 | implicit val system = ActorSystem("on-spray-can")
10 |
11 | lazy val oauth = system.actorOf(Props[OAuth2Actor])
12 |
13 | startServer(interface = "localhost", port = 8080) {
14 | pathPrefix("oauth") { ctx => oauth ! ctx }
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/scala/com/hasanozgan/demo/inmemory/oauth2/OAuth2Actor.scala:
--------------------------------------------------------------------------------
1 | package org.netology.spray.rest.oauth2
2 |
3 | import com.hasanozgan.demo.inmemory.oauth2.routes.ApiRoutes
4 | import org.netology.spray.rest.oauth2.routes._
5 | import org.netology.spray.rest.oauth2.utils.CustomRejectionHandler
6 | import akka.actor.Actor
7 | import spray.oauth.adapters.inmemory.SprayOAuth2Support
8 | import spray.oauth.endpoints.OAuth2Services
9 | import spray.routing._
10 | import spray.http.StatusCodes._
11 | import spray.routing.Directive.pimpApply
12 | import spray.util.LoggingContext
13 | import spray.http.HttpRequest
14 | import spray.http.HttpResponse
15 | import spray.http.Timedout
16 |
17 | /**
18 | * Created by hasanozgan on 01/03/14.
19 | */
20 | class OAuth2Actor extends Actor with SprayOAuth2Support with OAuth2Services with IndexRoutes with ApiRoutes with CustomRejectionHandler {
21 |
22 | // the HttpService trait defines only one abstract member, which
23 | // connects the services environment to the enclosing actor or test
24 | def actorRefFactory = context
25 |
26 | // this actor only runs our route, but you could add
27 | // other things here, like request stream processing
28 | // or timeout handling
29 | def receive = handleTimeouts orElse runRoute(handleRejections(myRejectionHandler)(handleExceptions(myExceptionHandler)(defaultOAuth2Routes ~ apiRoutes ~ initRoutes)))
30 |
31 | def handleTimeouts: Receive = {
32 | case Timedout(x: HttpRequest) =>
33 | sender ! HttpResponse(InternalServerError, "Something is taking way too long.")
34 | }
35 |
36 | implicit def myExceptionHandler(implicit log: LoggingContext) =
37 | ExceptionHandler.apply {
38 | case e: Exception => {
39 | complete(InternalServerError, e.getMessage)
40 | }
41 |
42 | }
43 | }
44 |
45 | class SomeCustomException(msg: String) extends RuntimeException(msg)
46 |
47 |
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/scala/com/hasanozgan/demo/inmemory/oauth2/routes/APIRoutes.scala:
--------------------------------------------------------------------------------
1 | package com.hasanozgan.demo.inmemory.oauth2.routes
2 |
3 | import spray.oauth.adapters.inmemory.SprayOAuth2Support
4 | import spray.oauth.authentication.ResourceAuthenticator
5 | import spray.routing.HttpService
6 | import scala.concurrent.ExecutionContext.Implicits.global
7 |
8 | /**
9 | * Created by hasanozgan on 31/07/14.
10 | */
11 | trait ApiRoutes extends SprayOAuth2Support with ResourceAuthenticator with HttpService {
12 | val apiRoutes =
13 | path("user") {
14 | get {
15 | authenticate(tokenAuthenticator) { info =>
16 | authorize(allowedScopes(info, "membership", "membership.readonly")) {
17 | complete("Success")
18 | }
19 | }
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/scala/com/hasanozgan/demo/inmemory/oauth2/routes/IndexRoutes.scala:
--------------------------------------------------------------------------------
1 | package org.netology.spray.rest.oauth2.routes
2 |
3 | import spray.oauth.adapters.inmemory.models._
4 | import spray.oauth.adapters.inmemory.utils.Sequence
5 | import spray.routing.{ StandardRoute, HttpService }
6 | import spray.http.{ MediaTypes, StatusCodes }
7 | import com.github.nscala_time.time.Imports._
8 | import spray.oauth.utils.TokenGenerator
9 | import spray.oauth.models.GrantType
10 | import spray.oauth.models.GrantType._
11 | import spray.oauth.directives.SessionDirectives
12 | import MediaTypes._
13 |
14 | /**
15 | * Created by hasanozgan on 17/03/14.
16 | */
17 | trait IndexRoutes extends HttpService with SessionDirectives {
18 |
19 | val initRoutes =
20 | path("init") {
21 | get {
22 | complete {
23 | val defaultScopes = List[String]("membership", "membership.readonly")
24 | val defaultGrants = List[String](GrantType.AuthorizationCode, GrantType.ClientCredentials, GrantType.RefreshToken)
25 |
26 | val role = Role(Sequence.nextId, "default", defaultScopes, defaultGrants)
27 | RoleDAO.insert(role)
28 |
29 | val user = UserDAO.create("123456", "user", "pass")
30 |
31 | val consumer = Consumer(id = Sequence.nextId,
32 | fk_role = role.id,
33 | scopes = List("membership.internal"),
34 | grants = List[String](GrantType.Password),
35 | name = "activist",
36 | site_url = Some("http://localhost"), logo = None, description = None,
37 | callback_url = Some("http://localhost/callback"),
38 | client_secret = TokenGenerator.bearer)
39 |
40 | ConsumerDAO.insert(consumer).toString
41 |
42 | s"Success ${consumer.id} - ${consumer.client_secret}"
43 | }
44 |
45 | }
46 | } ~
47 | path("logout") {
48 | get {
49 | deleteSession {
50 | redirect("/oauth2", StatusCodes.TemporaryRedirect)
51 | }
52 | }
53 | } ~
54 | path("login") {
55 | post {
56 | parameters('continue ? "") { continue: String =>
57 | complete {
58 | s"redirect ${continue}"
59 | }
60 | }
61 | } ~
62 | get {
63 | createSession {
64 | respondWithMediaType(`text/html`) {
65 | complete {
66 |
67 |
68 | HELLO SALAT
69 |
70 |
71 | }
72 | }
73 |
74 | /*
75 | completeWithTemplate("auth/login") {
76 | Map(
77 | "name" -> "Chris",
78 | "value" -> 10000,
79 | "taxed_value" -> (10000 - (10000 * 0.18)),
80 | "in_ca" -> true
81 | )
82 | }*/
83 | }
84 | }
85 | } ~
86 | pathPrefix("assets") {
87 | compressResponse() {
88 | getFromResourceDirectory("webapp")
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/scala/com/hasanozgan/demo/inmemory/oauth2/utils/CustomRejectionHandler.scala:
--------------------------------------------------------------------------------
1 | package org.netology.spray.rest.oauth2.utils
2 |
3 | import spray.routing._
4 | import spray.http._
5 | import java.net.URLEncoder
6 |
7 | trait CustomRejectionHandler extends HttpService {
8 | implicit val myRejectionHandler = RejectionHandler {
9 | case SecurePageRejection(uri) :: _ => redirectWithUri(uri)
10 | //case _ => complete(StatusCodes.BadRequest, "Something went wrong here")
11 | }
12 |
13 | def redirectWithUri(uri: Uri): StandardRoute = {
14 | val encodedUri = URLEncoder.encode(uri.toString(), "UTF-8")
15 | redirect(s"https://localhost/oauth2/login?continue=${encodedUri}", StatusCodes.TemporaryRedirect)
16 | }
17 | }
--------------------------------------------------------------------------------
/samples/inmemory-webapp/src/main/scala/com/hasanozgan/demo/inmemory/oauth2/utils/SecurePageRejection.scala:
--------------------------------------------------------------------------------
1 | package org.netology.spray.rest.oauth2.utils
2 |
3 | import spray.routing.{ Rejection, AuthenticationFailedRejection }
4 | import spray.http.{ Uri, HttpHeader }
5 |
6 | /**
7 | * Created with IntelliJ IDEA.
8 | * User: hasan.ozgan
9 | * Date: 4/7/14
10 | * Time: 5:11 PM
11 | * To change this template use File | Settings | File Templates.
12 | */
13 | case class SecurePageRejection(uri: Uri) extends Rejection
14 |
--------------------------------------------------------------------------------