Google+ Sign-In with Scalatra

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='&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

Related Blogs

Get content like this straight to your inbox!

Software is our passion.

We are software craftspeople. We build well-crafted software for our clients, we help developers to get better at their craft through training, coaching and mentoring, and we help companies get better at delivering software.

Company Registration No: 8712584