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.
In this project we are using:
First we need to add an AuthenticationFilter to our Scalatra application.
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.
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.
Then we need a sign-in page, that is displayed when the user is not authenticated.
-@ 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'); } }); } }
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".
We then need a controller that will respond to all these requests, displays the respective pages, and do the authorisation.
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>
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.
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.
That's about it. You now can display the name of the user on all your pages, using a default layout.
!!! html head title= title body header div span Hello #{user.name.displayName} div h1= headline != body
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.