The requirements
For one of our internal pet-projects at Codurance, we decided to have authentication and authorisation using Google+ Sign-in . Google+ Sign-In is able to authenticate anyone with a Google email account (gmail or business) using OAuth 2.0. However, we wanted to restrict the application to Codurance craftsmen only, that means, people with a Codurance email address.
The application had also to redirect us to the desired URL, in case we tried to access a deep URL without being authenticated.
Technology stack
In this project we are using:
Scala
Scalatra as a web micro-framework
Jade as template engine
sbt as our build tool.
json4s for JSON manipulation
Newman as HTTP client library
Implementation
Authentication Filter
First we need to add an AuthenticationFilter to our Scalatra application.
import javax.servlet.ServletContext
import com.codurance.cerebro.controllers.MainController import com.codurance.cerebro.security.AuthenticationFilter import org.scalatra._
class ScalatraBootstrap extends LifeCycle { override def init ( context : ServletContext ) { context . mount ( new AuthenticationFilter , "/" ) context . mount ( new MainController , "/ ") } }
Then, in the AuthenticationFilter, we need to redirect to the sign-in page when we don't have a user in the session. We also need to exclude the pages and URLs that don't need a user to be logged in.
> package com.codurance.cerebro.security
import org.scalatra.ScalatraFilter
class AuthenticationFilter extends ScalatraFilter { before () { if ( isProtectedUrl && userIsNotAuthenticated ) { redirect ( "/signin?originalUri=" + originalURL ) } }
<span class ="k" >def</span> <span class ="nf" >originalURL</span><span class ="o" >()</span><span class ="k" >:</span> <span class ="kt" >String</span> <span class ="o" >=</span> <span class ="o" >{</span>
<span class ="k" >val</span> <span class ="nv" >url</span> <span class ="k" >=</span> <span class ="nc" >Option</span><span class ="o" >(</span><span class ="nv" >request</span><span class ="o" >.</span><span class ="py" >getRequestURI</span><span class ="o" >).</span><span class ="py" >getOrElse</span><span class ="o" >(</span><span class ="s" >"/main" </span><span class ="o" >)</span>
<span class ="nf" >if </span> <span class ="o" >(</span><span class ="nv" >url</span><span class ="o" >.</span><span class ="py" >startsWith</span><span class ="o" >(</span><span class ="s" >"/signin" </span><span class ="o" >))</span> <span class ="s" >"/main" </span> <span class ="k" >else </span> <span class ="n" >url</span>
<span class ="o" >}</span>
<span class ="k" >def</span> <span class ="nf" >userIsNotAuthenticated</span><span class ="k" >:</span> <span class ="kt" >Boolean</span> <span class ="o" >=</span> <span class ="o" >{</span>
<span class ="nv" >request</span><span class ="o" >.</span><span class ="py" >getSession</span><span class ="o" >.</span><span class ="py" >getAttribute</span><span class ="o" >(</span><span class ="s" >"user" </span><span class ="o" >)</span> <span class ="o" >==</span> <span class ="kc" >null </span>
<span class ="o" >}</span>
<span class ="k" >def</span> <span class ="nf" >isProtectedUrl</span><span class ="o" >()</span><span class ="k" >:</span> <span class ="kt" >Boolean</span> <span class ="o" >=</span> <span class ="o" >{</span>
<span class ="k" >val</span> <span class ="nv" >url</span> <span class ="k" >=</span> <span class ="nv" >request</span><span class ="o" >.</span><span class ="py" >getRequestURI</span><span class ="o" >();</span>
<span class ="o" >!(</span><span class ="nv" >url</span><span class ="o" >.</span><span class ="py" >equals </span><span class ="o" >(</span><span class ="s" >"/signin" </span><span class ="o" >)</span> <span class ="o" >||</span> <span class ="nv" >url</span><span class ="o" >.</span><span class ="py" >equals </span><span class ="o" >(</span><span class ="s" >"/authorise" </span><span class ="o" >)</span> <span class ="o" >||</span> <span class ="nv" >url</span><span class ="o" >.</span><span class ="py" >equals </span><span class ="o" >(</span><span class ="s" >"/not-authorised" </span><span class ="o" >))</span>
<span class ="o" >}</span>
For more information about filters, check the Scalatra documentation.
signin.jade
Then we need a sign-in page, that is displayed when the user is not authenticated.
> - attributes("title") = "Cerebro" - attributes("layout") = "/WEB-INF/templates/layouts/no-header.jade"
-@ val originalUri: String
h1 Welcome to Cerebro!
p= "Please sigin in using google id!" p URI: #{originalUri}
:!javascript function onSignInCallback(authResult) { if (authResult['access_token']) { $.ajax({ type: 'POST', url: '/authorise', contentType: 'application/x-www-form-urlencoded; charset=utf-8', data: {authCode: authResult.code }, success: function(result) { window.location.replace('#{originalUri}'); }, error: function(result) { window.location.replace('/not-authorised'); } }); } }
gConnect
button(class ='g -signin '
data -scope='https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email'
data -requestvisibleactions='http://schemas.google.com/AddActivity'
data -clientId='<<YOUR_CLIENT_ID>>'
data -accesstype='offline' data -callback='onSignInCallback'
data -theme='dark'
data -cookiepolicy='single_host_origin' )
If you are not using Jade or want more details, check the official documentation about how to add the sign-in button to your page.
This should be enough to trigger the Google authentication form when clicking on the Sign-In button. Once the authentication is done, the callback function will send us a POST with the "authCode".
Main Controller
We then need a controller that will respond to all these requests, displays the respective pages, and do the authorisation.
> package com.codurance.cerebro.controllers
import javax.servlet.http. { HttpServletResponse , HttpServletRequest }
class BaseController extends CerebroStack {
<span class ="k" >def</span> <span class ="nf" >display</span><span class ="o" >(</span><span class ="n" >page</span><span class ="k" >:</span> <span class ="kt" >String</span><span class ="o" >,</span> <span class ="n" >attributes</span><span class ="k" >:</span> <span class ="o" >(</span><span class ="kt" >String</span><span class ="o" >,</span> <span class ="kt" >Any</span><span class ="o" >)*)(</span><span class ="k" >implicit </span> <span class ="n" >request</span><span class ="k" >:</span> <span class ="kt" >HttpServletRequest</span><span class ="o" >,</span> <span class ="n" >response</span><span class ="k" >:</span> <span class ="kt" >HttpServletResponse</span><span class ="o" >)</span><span class ="k" >:</span> <span class ="kt" >String</span> <span class ="o" >=</span> <span class ="o" >{</span>
<span class ="n" >contentType</span> <span class ="k" >=</span> <span class ="s" >"text/html" </span>
<span class ="k" >val</span> <span class ="nv" >all_attributes</span> <span class ="k" >=</span> <span class ="n" >attributes</span> <span class ="o" >:+</span> <span class ="o" >(</span><span class ="s" >"user" </span><span class ="o" >,</span> <span class ="nv" >session</span><span class ="o" >.</span><span class ="py" >getAttribute</span><span class ="o" >(</span><span class ="s" >"user" </span><span class ="o" >))</span>
<span class ="nf" >jade</span><span class ="o" >(</span><span class ="n" >page</span><span class ="o" >,</span> <span class ="n" >all_attributes</span><span class ="k" >:</span> <span class ="k" >_</span><span class ="kt" >*</span><span class ="o" >)</span>
<span class ="o" >}</span>
> package com.codurance.cerebro.controllers
import com.codurance.cerebro.security.CoduranceAuthorisation.authorise
import scala.Predef._
class MainController extends BaseController {
<span class ="nf" >get </span><span class ="o" >(</span><span class ="s" >"/" </span><span class ="o" >)</span> <span class ="o" >{</span>
<span class ="nf" >display</span><span class ="o" >(</span><span class ="s" >"main" </span><span class ="o" >)</span>
<span class ="o" >}</span>
<span class ="nf" >get </span><span class ="o" >(</span><span class ="s" >"/main" </span><span class ="o" >)</span> <span class ="o" >{</span>
<span class ="nf" >display</span><span class ="o" >(</span><span class ="s" >"main" </span><span class ="o" >)</span>
<span class ="o" >}</span>
<span class ="nf" >get </span><span class ="o" >(</span><span class ="s" >"/signin" </span><span class ="o" >)</span> <span class ="o" >{</span>
<span class ="nf" >display</span><span class ="o" >(</span><span class ="s" >"signin" </span><span class ="o" >,</span> <span class ="s" >"originalUri" </span> <span class ="o" >-></span> <span class ="nv" >request</span><span class ="o" >.</span><span class ="py" >getParameter</span><span class ="o" >(</span><span class ="s" >"originalUri" </span><span class ="o" >))</span>
<span class ="o" >}</span>
<span class ="nf" >get </span><span class ="o" >(</span><span class ="s" >"/not-authorised" </span><span class ="o" >)</span> <span class ="o" >{</span>
<span class ="nf" >display</span><span class ="o" >(</span><span class ="s" >"not-authorised" </span><span class ="o" >)</span>
<span class ="o" >}</span>
<span class ="nf" >post</span><span class ="o" >(</span><span class ="s" >"/authorise" </span><span class ="o" >)</span> <span class ="o" >{</span>
<span class ="k" >val</span> <span class ="nv" >authCode</span><span class ="k" >:</span> <span class ="kt" >String</span> <span class ="o" >=</span> <span class ="nv" >params </span><span class ="o" >.</span><span class ="py" >getOrElse</span><span class ="o" >(</span><span class ="s" >"authCode" </span><span class ="o" >,</span> <span class ="nf" >halt</span><span class ="o" >(</span><span class ="mi" >400 </span><span class ="o" >))</span>
<span class ="nf" >authorise</span><span class ="o" >(</span><span class ="n" >authCode</span><span class ="o" >)</span>
<span class ="o" >}</span>
The MainController responds to "/authorise", which invokes the authorisation function defined inside CoduranceAuthorisation. Note that we receive the "authCode" from the Google+ authentication. Once the user was authenticated, we had to make the application available just for users using a Codurance email. For that, we had to invoke the Google+ People API to get more information (email address, domain, etc).
The authorise function would then check if the user belongs to the Codurance domain and add her to the session.
> package com.codurance.cerebro.security
import java.net.URL import javax.servlet.http. { HttpSession , HttpServletResponse , HttpServletRequest } import javax.servlet.http.HttpServletResponse._
import com.google.api.client.googleapis.auth.oauth2. { GoogleAuthorizationCodeTokenRequest , GoogleTokenResponse } import com.google.api.client.http.javanet.NetHttpTransport import com.google.api.client.json.jackson.JacksonFactory import com.stackmob.newman. import com.stackmob.newman.dsl.
import scala.concurrent.Await import scala.concurrent.duration._
object CoduranceAuthorisation {
<span class ="k" >implicit </span> <span class ="k" >val</span> <span class ="nv" >httpClient</span> <span class ="k" >=</span> <span class ="k" >new </span> <span class ="nc" >ApacheHttpClient</span>
<span class ="k" >val</span> <span class ="nv" >GOOGLE_PLUS_PEOPLE_URL</span> <span class ="k" >=</span> <span class ="s" >"https://www.googleapis.com/plus/v1/people/me?fields=aboutMe%2Ccover%2FcoverPhoto%2CdisplayName%2Cdomain%2Cemails%2Clanguage%2Cname&access_token=" </span>
<span class ="k" >val</span> <span class ="nv" >CLIENT_ID</span><span class ="k" >:</span> <span class ="kt" >String</span> <span class ="o" >=</span> <span class ="s" >"<<YOUR_CLIENT_ID>>" </span>
<span class ="k" >val</span> <span class ="nv" >CLIENT_SECRET</span> <span class ="k" >=</span> <span class ="s" >"<<YOUR_CLIENT_SECRET>>" </span>
<span class ="k" >val</span> <span class ="nv" >API_KEY</span> <span class ="k" >=</span> <span class ="s" >"<<YOUR_API_KEY>>" </span>
<span class ="k" >val</span> <span class ="nv" >APPLICATION_NAME</span> <span class ="k" >=</span> <span class ="s" >"<<YOUR_APP_NAME>>" </span>
<span class ="k" >val</span> <span class ="nv" >JSON_FACTORY</span> <span class ="k" >=</span> <span class ="k" >new </span> <span class ="nc" >JacksonFactory</span><span class ="o" >()</span>
<span class ="k" >val</span> <span class ="nv" >TRANSPORT</span> <span class ="k" >=</span> <span class ="k" >new </span> <span class ="nc" >NetHttpTransport</span><span class ="o" >()</span>
<span class ="k" >def</span> <span class ="nf" >authorise</span><span class ="o" >(</span><span class ="n" >authCode</span><span class ="k" >:</span> <span class ="kt" >String</span><span class ="o" >)(</span><span class ="k" >implicit </span> <span class ="n" >session</span><span class ="k" >:</span> <span class ="kt" >HttpSession</span><span class ="o" >,</span> <span class ="n" >response</span><span class ="k" >:</span> <span class ="kt" >HttpServletResponse</span><span class ="o" >)</span><span class ="k" >:</span> <span class ="kt" >Unit</span> <span class ="o" >=</span> <span class ="o" >{</span>
<span class ="k" >val</span> <span class ="nv" >user</span> <span class ="k" >=</span> <span class ="nf" >userFor</span><span class ="o" >(</span><span class ="n" >authCode</span><span class ="o" >)</span>
<span class ="nv" >user</span><span class ="o" >.</span><span class ="py" >domain</span> <span class ="k" >match</span> <span class ="o" >{</span>
<span class ="k" >case </span> <span class ="nc" >Some</span><span class ="o" >(</span><span class ="nc" >Domain</span><span class ="o" >(</span><span class ="s" >"codurance.com" </span><span class ="o" >))</span> <span class ="k" >=></span> <span class ="o" >{</span>
<span class ="nv" >session</span><span class ="o" >.</span><span class ="py" >setAttribute</span><span class ="o" >(</span><span class ="s" >"user" </span><span class ="o" >,</span> <span class ="n" >user</span><span class ="o" >)</span>
<span class ="nv" >response</span><span class ="o" >.</span><span class ="py" >setStatus</span><span class ="o" >(</span><span class ="nc" >SC_OK</span><span class ="o" >)</span>
<span class ="o" >}</span>
<span class ="k" >case </span> <span class ="k" >_</span> <span class ="k" >=></span> <span class ="nv" >response</span><span class ="o" >.</span><span class ="py" >setStatus</span><span class ="o" >(</span><span class ="nc" >SC_UNAUTHORIZED</span><span class ="o" >)</span>
<span class ="o" >}</span>
<span class ="o" >}</span>
<span class ="k" >def</span> <span class ="nf" >userFor</span><span class ="o" >(</span><span class ="n" >authCode</span><span class ="k" >:</span> <span class ="kt" >String</span><span class ="o" >)</span><span class ="k" >:</span> <span class ="kt" >User</span> <span class ="o" >=</span> <span class ="o" >{</span>
<span class ="k" >val</span> <span class ="nv" >tokenResponse</span><span class ="k" >:</span> <span class ="kt" >GoogleTokenResponse</span> <span class ="o" >=</span>
<span class ="k" >new </span> <span class ="nc" >GoogleAuthorizationCodeTokenRequest</span><span class ="o" >(</span>
<span class ="nc" >TRANSPORT</span><span class ="o" >,</span> <span class ="nc" >JSON_FACTORY</span><span class ="o" >,</span> <span class ="nc" >CLIENT_ID</span><span class ="o" >,</span> <span class ="nc" >CLIENT_SECRET</span><span class ="o" >,</span> <span class ="n" >authCode</span><span class ="o" >,</span> <span class ="s" >"postmessage" </span>
<span class ="o" >).</span><span class ="py" >execute</span>
<span class ="k" >val</span> <span class ="nv" >url</span> <span class ="k" >=</span> <span class ="k" >new </span> <span class ="nc" >URL</span><span class ="o" >(</span><span class ="nc" >GOOGLE_PLUS_PEOPLE_URL</span> <span class ="o" >+</span> <span class ="nv" >tokenResponse</span><span class ="o" >.</span><span class ="py" >getAccessToken</span><span class ="o" >)</span>
<span class ="k" >val</span> <span class ="nv" >userInfo</span> <span class ="k" >=</span> <span class ="nv" >Await</span><span class ="o" >.</span><span class ="py" >result</span><span class ="o" >(</span><span class ="nc" >GET</span><span class ="o" >(</span><span class ="n" >url</span><span class ="o" >).</span><span class ="py" >apply</span><span class ="o" >,</span> <span class ="mf" >10. </span><span class ="n" >seconds</span><span class ="o" >)</span>
<span class ="nv" >GooglePlusJSONResponseParser</span><span class ="o" >.</span><span class ="py" >toUser</span><span class ="o" >(</span><span class ="nv" >userInfo</span><span class ="o" >.</span><span class ="py" >bodyString</span><span class ="o" >,</span> <span class ="nv" >tokenResponse</span><span class ="o" >.</span><span class ="py" >toString</span><span class ="o" >)</span>
<span class ="o" >}</span>
Note that in the GOOGLE_PLUS_PEOPLE_URL we specify all the fields we are interested in, including the domain and emails .
GooglePlusJSONResponseParser is a class that we created to parse the JSON response and convert into a User object. We are not showing it in order to keep this post short and focused. You can create your own JSON parser. :)
IMPORTANT: Don't forget to import add the Google+ APIs to your sbt build file.
"com.google.apis" % "google-api-services-oauth2" % "v2-rev59-1.17.0-rc" ,
"com.google.apis" % "google-api-services-plus" % "v1-rev115-1.17.0-rc" ,
That's about it. You now can display the name of the user on all your pages, using a default layout.
-@ val title : String
-@ val headline : String = title
-@ val body : String
-@ val user : com.codurance.cerebro.security.User
!!! html head title = title body header div span Hello #{ user . name . displayName } div h1 = headline != body