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 |
Use the cog icon at the top right of the github main page to access your settings.
30 |
Choose Applications from the left settings navigation panel.
31 |
Under Developer Applications at the top, hit the "Register new application" button.
32 |
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.
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 |
48 |
Now hit the Register Application button
49 |
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.
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 |
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 |
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 |