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:
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