Google+ Sign-In with Scalatra

    10 Jul 2014
    Sandro Mancuso, Co-founder and Group CEO

    Sandro Mancuso, Co-founder and Group CEO

    See author's bio and posts

    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='&lt;&lt;YOUR_CLIENT_ID&gt;&gt;'
    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">-&gt;</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&amp;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">"&lt;&lt;YOUR_CLIENT_ID&gt;&gt;"</span>
    <span class="k">val</span> <span class="nv">CLIENT_SECRET</span> <span class="k">=</span> <span class="s">"&lt;&lt;YOUR_CLIENT_SECRET&gt;&gt;"</span>
    <span class="k">val</span> <span class="nv">API_KEY</span> <span class="k">=</span> <span class="s">"&lt;&lt;YOUR_API_KEY&gt;&gt;"</span>
    <span class="k">val</span> <span class="nv">APPLICATION_NAME</span> <span class="k">=</span> <span class="s">"&lt;&lt;YOUR_APP_NAME&gt;&gt;"</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">=&gt;</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">=&gt;</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