├── public ├── stylesheets │ └── main.css ├── images │ └── favicon.png └── javascripts │ └── hello.js ├── project ├── build.properties └── plugins.sbt ├── app ├── views │ ├── index.scala.html │ └── main.scala.html ├── controllers │ └── Application.scala └── util │ └── OAuth2.scala ├── activator.properties ├── .gitignore ├── conf ├── routes └── application.conf ├── LICENSE ├── test ├── IntegrationSpec.scala └── ApplicationSpec.scala ├── README └── tutorial └── index.html /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickwall/activator-play-oauth2-scala/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Mon Sep 15 14:18:15 PDT 2014 3 | template.uuid=9d0f021d-ca8f-4a88-992f-f6468442419e 4 | sbt.version=0.13.5 5 | -------------------------------------------------------------------------------- /app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String, redirectUrl: String) 2 | 3 | @main("Welcome to Play") { 4 |

Not Yet Authenticated

5 | Log in with GITHUB! 6 | } 7 | -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | name=play-oauth2-scala 2 | title=Play Framework with OAuth2 API sample 3 | description=A starter application with Play Framework using OAuth2 to access a ReST API 4 | tags=playframework,auth2,scala,rest,api,github 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | project/activator-sbt* 5 | target 6 | activator 7 | activator.bat 8 | activator-launch*.jar 9 | activator-sbt-echo-akka-shim.sbt 10 | tmp 11 | .history 12 | dist 13 | /.idea 14 | /*.iml 15 | /out 16 | /.idea_modules 17 | /.classpath 18 | /.project 19 | /RUNNING_PID 20 | /.settings 21 | -------------------------------------------------------------------------------- /app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | 8 | # Map static resources from the /public folder to the /assets URL path 9 | GET /assets/*file controllers.Assets.at(path="/public", file) 10 | 11 | # OAuth2 Stuff 12 | GET /_oauth-callback util.OAuth2.callback(code: Option[String], state: Option[String]) 13 | GET /_oauth-success util.OAuth2.success -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with 4 | the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 5 | 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an 7 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 8 | language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 2 | 3 | // The Play plugin 4 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.4") 5 | 6 | // web plugins 7 | 8 | addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0") 9 | 10 | addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.0") 11 | 12 | addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.1") 13 | 14 | addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.1") 15 | 16 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.0.0") 17 | 18 | addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.0.0") 19 | -------------------------------------------------------------------------------- /test/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * add your integration spec here. 10 | * An integration test will fire up a whole play application in a real (or headless) browser 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class IntegrationSpec extends Specification { 14 | 15 | "Application" should { 16 | 17 | "work from within a browser" in new WithBrowser { 18 | 19 | browser.goTo("http://localhost:" + port) 20 | 21 | browser.pageSource must contain("Your new application is ready.") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import java.util.UUID 4 | 5 | import play.api._ 6 | import play.api.mvc._ 7 | import util.OAuth2 8 | 9 | object Application extends Controller { 10 | 11 | def index = Action { implicit request => 12 | val oauth2 = new OAuth2(Play.current) 13 | val callbackUrl = util.routes.OAuth2.callback(None, None).absoluteURL() 14 | val scope = "repo" // github scope - request repo access 15 | val state = UUID.randomUUID().toString // random confirmation string 16 | val redirectUrl = oauth2.getAuthorizationUrl(callbackUrl, scope, state) 17 | Ok(views.html.index("Your new application is ready.", redirectUrl)). 18 | withSession("oauth-state" -> state) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | OAuth2 Login and API with Play and Scala 2 | ======================================== 3 | 4 | An example of using github to authorize login and also use the github API with an 5 | auth token. This is a typesafe activator template with a tutorial. If you aren't sure 6 | what that is, please look at 7 | 8 | https://typesafe.com/activator 9 | 10 | for more details. In a nutshell, to use this project: 11 | 12 | - Download Typesafe Activator from the above link 13 | - Run activator using "activator ui" 14 | - Clone this project (or find it and open it in Activator 15 | with a search for play-oauth2-scala) 16 | - On the right of the activator UI, follow the tutorial. Hit > at the 17 | top of the tutorial pane when you are ready the next page. Blue links 18 | are active and take you to external resources or location in the code. -------------------------------------------------------------------------------- /test/ApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | import org.specs2.mutable._ 2 | import org.specs2.runner._ 3 | import org.junit.runner._ 4 | 5 | import play.api.test._ 6 | import play.api.test.Helpers._ 7 | 8 | /** 9 | * Add your spec here. 10 | * You can mock out a whole application including requests, plugins etc. 11 | * For more information, consult the wiki. 12 | */ 13 | @RunWith(classOf[JUnitRunner]) 14 | class ApplicationSpec extends Specification { 15 | 16 | "Application" should { 17 | 18 | "send 404 on a bad request" in new WithApplication{ 19 | route(FakeRequest(GET, "/boum")) must beNone 20 | } 21 | 22 | "render the index page" in new WithApplication{ 23 | val home = route(FakeRequest(GET, "/")).get 24 | 25 | status(home) must equalTo(OK) 26 | contentType(home) must beSome.which(_ == "text/html") 27 | contentAsString(home) must contain ("Your new application is ready.") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # 8 | # This must be changed for production, but we recommend not changing it in this file. 9 | # 10 | # See http://www.playframework.com/documentation/latest/ApplicationSecret for more details. 11 | application.secret="mnf6vyGDg1RWR0`ViDJjI;_sV=`0EvqeDsIX8HObxVc]c=y?ZT=@C4t7tv<;D?mQ" 12 | 13 | # The application languages 14 | # ~~~~~ 15 | application.langs="en" 16 | 17 | # Global object class 18 | # ~~~~~ 19 | # Define the Global object class for this application. 20 | # Default to Global in the root package. 21 | # application.global=Global 22 | 23 | # Router 24 | # ~~~~~ 25 | # Define the Router object to use for this application. 26 | # This router will be looked up first when the application is starting up, 27 | # so make sure this is the entry point. 28 | # Furthermore, it's assumed your route file is named properly. 29 | # So for an application router like `my.application.Router`, 30 | # you may need to define a router file `conf/my.application.routes`. 31 | # Default to Routes in the root package (and conf/routes) 32 | # application.router=my.application.Routes 33 | 34 | # Database configuration 35 | # ~~~~~ 36 | # You can declare as many datasources as you want. 37 | # By convention, the default datasource is named `default` 38 | # 39 | # db.default.driver=org.h2.Driver 40 | # db.default.url="jdbc:h2:mem:play" 41 | # db.default.user=sa 42 | # db.default.password="" 43 | 44 | # Evolutions 45 | # ~~~~~ 46 | # You can disable evolutions if needed 47 | # evolutionplugin=disabled 48 | 49 | # Logger 50 | # ~~~~~ 51 | # You can also configure logback (http://logback.qos.ch/), 52 | # by providing an application-logger.xml file in the conf directory. 53 | 54 | # Root logger: 55 | logger.root=ERROR 56 | 57 | # Logger used by the framework: 58 | logger.play=INFO 59 | 60 | # Logger provided to your application: 61 | logger.application=DEBUG 62 | 63 | # env vars for github auth 64 | github.client.id=${GITHUB_AUTH_ID} 65 | github.client.secret=${GITHUB_AUTH_SECRET} 66 | 67 | github.redirect.url="https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s&state=%s" -------------------------------------------------------------------------------- /app/util/OAuth2.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import play.api.Application 4 | import play.api.Play 5 | import play.api.http.{MimeTypes, HeaderNames} 6 | import play.api.libs.ws.WS 7 | import play.api.mvc.{Results, Action, Controller} 8 | 9 | import scala.concurrent.Future 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | 12 | class OAuth2(application: Application) { 13 | lazy val githubAuthId = application.configuration.getString("github.client.id").get 14 | lazy val githubAuthSecret = application.configuration.getString("github.client.secret").get 15 | 16 | def getAuthorizationUrl(redirectUri: String, scope: String, state: String): String = { 17 | val baseUrl = application.configuration.getString("github.redirect.url").get 18 | baseUrl.format(githubAuthId, redirectUri, scope, state) 19 | } 20 | 21 | def getToken(code: String): Future[String] = { 22 | val tokenResponse = WS.url("https://github.com/login/oauth/access_token")(application). 23 | withQueryString("client_id" -> githubAuthId, 24 | "client_secret" -> githubAuthSecret, 25 | "code" -> code). 26 | withHeaders(HeaderNames.ACCEPT -> MimeTypes.JSON). 27 | post(Results.EmptyContent()) 28 | 29 | tokenResponse.flatMap { response => 30 | (response.json \ "access_token").asOpt[String].fold(Future.failed[String](new IllegalStateException("Sod off!"))) { accessToken => 31 | Future.successful(accessToken) 32 | } 33 | } 34 | } 35 | } 36 | 37 | object OAuth2 extends Controller { 38 | lazy val oauth2 = new OAuth2(Play.current) 39 | 40 | def callback(codeOpt: Option[String] = None, stateOpt: Option[String] = None) = Action.async { implicit request => 41 | (for { 42 | code <- codeOpt 43 | state <- stateOpt 44 | oauthState <- request.session.get("oauth-state") 45 | } yield { 46 | if (state == oauthState) { 47 | oauth2.getToken(code).map { accessToken => 48 | Redirect(util.routes.OAuth2.success()).withSession("oauth-token" -> accessToken) 49 | }.recover { 50 | case ex: IllegalStateException => Unauthorized(ex.getMessage) 51 | } 52 | } 53 | else { 54 | Future.successful(BadRequest("Invalid github login")) 55 | } 56 | }).getOrElse(Future.successful(BadRequest("No parameters supplied"))) 57 | } 58 | 59 | def success() = Action.async { request => 60 | implicit val app = Play.current 61 | request.session.get("oauth-token").fold(Future.successful(Unauthorized("No way Jose"))) { authToken => 62 | WS.url("https://api.github.com/user/repos"). 63 | withHeaders(HeaderNames.AUTHORIZATION -> s"token $authToken"). 64 | get().map { response => 65 | Ok(response.json) 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | OAuth2 Github Login and API Example in Play 4 | 5 | 6 |
7 |

Play, GitHub and OAuth2

8 | 9 |

10 | This template covers the use of OAuth2 to authenticate yourself into a play application, 11 | and furthermore using the OAuth2 credentials to access the github api. 12 |

13 |

14 | Before you can use this template successfully, you will need to configure the necessary 15 | github api key in github, and then restart activator with the api key and secret in your 16 | environment so that the play configuration can pick it up. See the next page of the 17 | tutorial for step-by-step instructions for doing that. 18 |

19 | 20 |
21 |
22 |

Setting up Github API Keys

23 |

24 | First, log in to your github account from your browser, and then follow these instructions 25 | to prepare for this demo. 26 |

27 | 28 |
    29 |
  1. Use the cog icon at the top right of the github main page to access your settings.
  2. 30 |
  3. Choose Applications from the left settings navigation panel.
  4. 31 |
  5. Under Developer Applications at the top, hit the "Register new application" button.
  6. 32 |
  7. Fill in the form, suggested values are: 33 |
      34 |
    • Application Name: Play OAuth2 Scala Sample (Testing) - Testing is added 35 | because in the real world you would tend to have one app registered for development 36 | and testing, and another with different API keys for production.
    • 37 |
    • Homepage URL: Some URL to a site you own or maintain. In real usage 38 | this would be the home page URL of your web application. If you really have nothing 39 | to put here, just make something up like http://nothingheretesting.com
    • 40 |
    • Application Description: An example of using OAuth2 for login authorization and API access in Play and Scala.
    • 41 |
    • Authorization callback URL http://localhost:9000/_oauth-callback
    • 42 |
    43 | it's important to use the exact Authorization callback URL given here, as this corresponds 44 | to the correct URL to redirect to after successful login for running this application on 45 | your local machine. For a real application, this would redirect back to the _oauth-callback 46 | URL on the real web application (hence the need for a separate one for development/testing). 47 |
  8. 48 |
  9. Now hit the Register Application button
  10. 49 |
  11. On the success page, write down or copy your Client ID and Client Secret fields somewhere, 50 | as you will need these to make the rest of the demo work.
  12. 51 |
52 |
53 |
54 |

Re-run the Demo with Keys

55 |

56 | Now that you have the client ID and secret keys, you need to re-run 57 | activator with those settings available. There are various ways you 58 | can do this (like putting them into your environment variables and 59 | logging out and in), but the easiest is to stop activator, then 60 | re-run it as 61 |

62 | 63 |

 64 | GITHUB_AUTH_ID=*CLIENT_ID* GITHUB_AUTH_SECRET=*CLIENT_SECRET* activator ui
 65 |         
66 |

67 | replacing *CLIENT_ID* and *CLIENT_SECRET* with the values provided 68 | by github for your application. All of this command should be on one 69 | line so that the environment variables apply to this run of activator. 70 |

71 |

72 | To run the demo and make sure it now works, select Run in activator 73 | and then hit http://localhost:9000. 74 | You should see a message to login with github, hit that link and 75 | you will either go to a github login dialog to fill out, or 76 | if you are already signed in to github, you will be immediately taken 77 | to a page listing details about your github projects. If you are not 78 | logged in to github you will be taken to that details page after 79 | successful login. 80 |

81 |

82 | Note that OAuth2 doesn't make you log in to a github session if 83 | you are already logged in to that session. It picks up that 84 | existing authentication and uses it to prove that you are you. 85 | This makes it a very nice user experience for someone using 86 | credentials that they have already authenticated in this browser 87 | session. 88 |

89 |

90 | If you want to see the flow with the login page, using another 91 | browser tab, log out of your github session and then try using 92 | this app again, you will go through the whole flow, including login. 93 |

94 |

95 | Now let's take a look at the code and the OAuth2 flow. 96 |

97 |
98 |
99 |

OAuth2 Authentication

100 |

101 | First let's take a look at the 102 | application.conf 103 | configuration settings. 104 |

105 |

106 | The important lines are at the bottom of this file: 107 |

108 |

109 | github.client.id=${GITHUB_AUTH_ID}
110 | github.client.secret=${GITHUB_AUTH_SECRET}
111 | 
112 | github.redirect.url="https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=%s&state=%s"
113 |     
114 |

115 | The first two use those client ID and secret values we provided to activator when 116 | we re-ran it, and the third is the github authorization URL which will be filled in 117 | with the id and secret, as well as a redirect uri field and a state, more on those later. 118 |

119 |

120 | Next, let's look at the 121 | routes file. 122 | which has the following lines of interest: 123 |

124 |
GET        /_oauth-callback        util.OAuth2.callback(code: Option[String], state: Option[String])
125 | GET        /_oauth-success         util.OAuth2.success
126 |

127 | The first defines a callback URL that oauth2 will use to confirm that the user 128 | has been authenticated. You will notice that this is the same URL we put into 129 | the callback field when registering our application on github. The two fields, code 130 | and scope, are filled in by github on the callback and we will see what these do 131 | later in the tutorial. They are optional because we are also going to use 132 | play to provide this callback URL through the routes lookup and don't have codes to 133 | use when getting that. The second route is just a page that we will go to once the 134 | callback is successful, and that page will list all github project details. 135 |

136 |
137 |
138 |

Redirecting to/from Github Auth

139 |

140 | When you first hit the app, the routes file sends you to the 141 | index method in the 142 | Application controller. This first constructs an instance of the OAuth2 class using 143 | the application configuration. 144 |

145 |

Next it gets the callback URL from the routes file with 146 | util.routes.OAuth2.callback(None, None).absoluteURL(). The two 147 | Nones for the optional fields are just to give defaults - we don't want parameters 148 | in the URL, just the URL itself to redirect back to. 149 |

150 |

151 | val scope="repo" is a field that github uses, in this case it means 152 | request access to the repo scope so that we can perform api operations against the repo. 153 |

154 |

155 | The state on the next line is a randomly generated UUID. This will be used to ensure 156 | that the call back from github matches our random UUID and make sure someone else isn't 157 | doing something bad to try and get into our site. It's up to us to check the returned 158 | code and make sure it matches this one upon handling the callback. 159 |

160 |

161 | Then we construct the big uber URL which will call github with our redirect, scope and 162 | random UUID. That URL is then passed into the index page, and at the same time we store 163 | the random UUID in our session so that we can check against it when the callback comes. 164 |

165 |

166 | From the index page the passed in URL 167 | is used to send the user to github, where the authentication will occur. 168 |

169 |
170 |
171 |

Handling the callback

172 |

173 | Once the user clicks on the github URL from the index page, github takes over. It may 174 | be that the user is already logged in to github, in which case github immediately 175 | calls back to our own redirect with the necessary fields. If not, the user will be 176 | prompted to log in to github, after which the same callback URL back to us will be used. 177 |

178 |

179 | When the callback does come, the routes file pushes it to the 180 | callback method in util.OAuth2 181 | which takes the optional fields code and state. With a 182 | successful callback from github, these will both be there, but we should protect against 183 | them not being, which is why the whole method is protected in a for comprehension. 184 |

185 |

186 | Assuming the code and state are present, the method then checks that the returned state 187 | is the same as that stored in our session, if not, that constitutes an authorization 188 | failure and probably indicates someone is doing something bad. If it matches, the 189 | code is then used to communicate back to github to acquire an API token 190 | for this session that we can use to make API calls. This token will be different 191 | on each successful authentication against github, which protects github itself from 192 | spoofing attacks. The code to obtain the github token by using this code can be seen 193 | in the 194 | getToken method in the OAuth2 class. 195 | Compared with the rest of the OAuth2 handshake, getting a token using a code is pretty 196 | straightforward and should be clear from examining the getToken method. Once the 197 | token is obtained, it is put into the session and the success page called (back in the 198 | callback method). 199 |

200 |

201 | If any of these steps fail (i.e. missing parameters, state not matching, etc.), then 202 | authentication failures or bad request messages are generated. There is only one 203 | happy path through this function, and that's when the code and state are returned and 204 | the state matches that expected, and the token can be obtained successfully. 205 |

206 | 207 |
208 |
209 |

Success, at Last!

210 |

211 | Finally let's take a look at the 212 | success method in the OAuth2 object. 213 | This is deliberately super-simple, since the aim of the tutorial is to show you how to 214 | successfully negotiate an OAuth2 connection, but anyway, this method just calls the github 215 | API rest URL to get the repo details. To do so, it requests the oauth-token we stored 216 | in the session, and if not there we redirect to unauthorized again. Assuming it is there, 217 | the token is appended to the webservice request and this is how github knows we can 218 | access that rest API call. 219 |

220 |

221 | For simplicity, the results of this call are simply dumped out to the browser as JSON 222 | text. In a real application you would parse the results into structured data, and likely 223 | implement many other API calls to navigate around your github repositories. The point is 224 | that you can use the same oauth-token stored in the session to make those API calls in 225 | the same way demonstrated here. 226 |

227 |
228 | 229 | --------------------------------------------------------------------------------