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

Blogs relacionados

Get content like this straight to your inbox!

Software es nuestra pasión.

Somos Software Craftspeople. Construimos software bien elaborado para nuestros clientes, ayudamos a los/as desarrolladores/as a mejorar en su oficio a través de la formación, la orientación y la tutoría. Ayudamos a las empresas a mejorar en la distribución de software.

Contacto

3 Sutton Lane, planta 3
Londres, EC1M 5PU

Teléfono: +44 207 4902967

2 Mount Street
Manchester, M2 5WQ

Teléfono: +44 161 302 6795

Carrer de Pallars 99, planta 4, sala 41
Barcelona, 08018

Teléfono: +34 937 82 28 82

Correo electrónico: hello@codurance.es
Número de registro: 8712584