├── .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 | [![Build Status](https://api.travis-ci.org/hasanozgan/spray-oauth.svg?branch=master)](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 |
67 | ${renderScopes(form)} 68 | 69 | 70 |
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 | --------------------------------------------------------------------------------