{"id":2802,"date":"2016-09-07T21:16:51","date_gmt":"2016-09-07T21:16:51","guid":{"rendered":"http:\/\/www.nikola-breznjak.com\/blog\/?p=2802"},"modified":"2016-09-13T05:20:04","modified_gmt":"2016-09-13T05:20:04","slug":"raneto-google-oauth-login","status":"publish","type":"post","link":"https:\/\/nikola-breznjak.com\/blog\/javascript\/nodejs\/raneto-google-oauth-login\/","title":{"rendered":"Raneto Google OAuth login"},"content":{"rendered":"<h2>TL;DR<\/h2>\n<p><a href=\"http:\/\/raneto.com\/\">Raneto<\/a> allows only basic username\/password authentication, so I added Google OAuth support. This option can be turned on by setting the <code>googleoauth<\/code> option in the <code>config.default.js<\/code> file to <code>true<\/code>, and by supplying the OAuth config object as outlined in the guides below. Additionally, you can allow only emails from the certain domain to use the service with one config setting.<\/p>\n<p>The basic idea was taken from the <a href=\"https:\/\/github.com\/GoogleCloudPlatform\/nodejs-getting-started\/tree\/master\/4-auth\">Google Cloud Platform Node.js guide<\/a>.<\/p>\n<p>This has been submitted as a <a href=\"https:\/\/github.com\/gilbitron\/Raneto\/pull\/142\">pull request<\/a> on the official Raneto Github repository. This is my way of saying thanks to an awesome author of Raneto. <em>edit: 13.09.2016: The pull request was <a href=\"https:\/\/github.com\/gilbitron\/Raneto\/pull\/142\">approved and merged!<\/a>.<\/em><\/p>\n<h2>Steps on how to reproduce this on fresh copy<\/h2>\n<p>Below are the steps one needs to take to get this working on a fresh copy of Raneto. In case this won&#8217;t make it to the official repo, you can clone my fork <a href=\"https:\/\/github.com\/Hitman666\/Raneto\">here<\/a>. Just make sure you set your Google OAuth credentials properly (more about this in the <strong>X<\/strong> section).<\/p>\n<h3>Install packages via npm<\/h3>\n<p><em>Make sure you first <a href=\"http:\/\/docs.raneto.com\/install\/installing-raneto\">install Raneto dependencies<\/a> after you clone it.<\/em><\/p>\n<p>Install the following packages:<\/p>\n<ul>\n<li><code>npm install passport --save-dev<\/code><\/li>\n<li><code>npm install passport-google-oauth20 --save-dev<\/code><\/li>\n<\/ul>\n<h3>Editing the <code>app\/index.js<\/code> file<\/h3>\n<ul>\n<li>Add passport: <code>var passport=require('passport');<\/code> just after raneto is required.<\/li>\n<li>Add oauth2 middleware: <code>var oauth2= require('.\/middleware\/oauth2.js');<\/code> in the config block, just afer <code>error_handler.js<\/code> middleware.<\/li>\n<li>Change <code>secret<\/code> to <code>secret:config.secret,<\/code> in the <code>\/\/ HTTP Authentication<\/code> section.<\/li>\n<li>&gt;&gt;&gt; Remove the rn-login route <code>app.post('\/rn-login', route_login);<\/code><\/li>\n<li>&gt;&gt;&gt; Remove the logout route: <code>app.get('\/logout', route_logout);<\/code><\/li>\n<li>Add the following Oauth settings, just before the <code>app.post('\/rn-login', route_login);<\/code> line:<\/li>\n<\/ul>\n<pre><code>\/\/ OAuth2\nif (config.googleoauth === true) {\napp.use(passport.initialize());\napp.use(passport.session());\napp.use(oauth2.router(config));\napp.use(oauth2.template);\n}\n<\/code><\/pre>\n<ul>\n<li>Change the <code>Online Editor Routes<\/code> to look like this now:<\/li>\n<\/ul>\n<pre><code>\/\/ Online Editor Routes\nif (config.allow_editing === true) {\nif (config.googleoauth === true) {\napp.post('\/rn-edit', oauth2.required, route_page_edit);\napp.post('\/rn-delete', oauth2.required, route_page_delete);\napp.post('\/rn-add-page', oauth2.required, route_page_create);\napp.post('\/rn-add-category', oauth2.required, route_category_create);\n}\nelse {\napp.post('\/rn-edit', authenticate, route_page_edit);\napp.post('\/rn-delete', authenticate, route_page_delete);\napp.post('\/rn-add-page', authenticate, route_page_create);\napp.post('\/rn-add-category', authenticate, route_category_create);\n}\n}\n<\/code><\/pre>\n<ul>\n<li>Set the root routes to be like this:<\/li>\n<\/ul>\n<pre><code>\/\/ Router for \/ and \/index with or without search parameter\nif (config.googleoauth === true) {\napp.get('\/:var(index)?', oauth2.required, route_search, route_home);\napp.get(\/^([^.]*)\/, oauth2.required, route_wildcard);\n}\nelse {\napp.get('\/:var(index)?', route_search, route_home);\napp.get(\/^([^.]*)\/, route_wildcard);\n}\n<\/code><\/pre>\n<h3>Editing the <code>app\/middleware\/authenticate.js<\/code> file<\/h3>\n<p>Change the <code>res.redirect(403, '\/login');<\/code> line to be:<\/p>\n<pre><code>if (config.googleoauth === true) {\nres.redirect('\/login');\n}\nelse {\nres.redirect(403, '\/login');\n}\n<\/code><\/pre>\n<h3>Editing the <code>app\/routes\/login_page.route.js<\/code> file<\/h3>\n<p>Add the <code>googleoauth<\/code> variable to the return object like this:<\/p>\n<pre><code>return res.render('login', {\nlayout : null,\nlang : config.lang,\nrtl_layout : config.rtl_layout,\ngoogleoauth : config.googleoauth\n});\n<\/code><\/pre>\n<h3>Add the oauth2.js file<\/h3>\n<p>Create a new file <code>oauth2.js<\/code> in the <code>app\/middleware<\/code> folder with the following content:<\/p>\n<pre><code>\/\/ Copyright 2015-2016, Google, Inc.\n\/\/ Licensed under the Apache License, Version 2.0 (the \"License\");\n\/\/ you may not use this file except in compliance with the License.\n\/\/ You may obtain a copy of the License at\n\/\/\n\/\/ http:\/\/www.apache.org\/licenses\/LICENSE-2.0\n\/\/\n\/\/ Unless required by applicable law or agreed to in writing, software\n\/\/ distributed under the License is distributed on an \"AS IS\" BASIS,\n\/\/ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\/\/ See the License for the specific language governing permissions and\n\/\/ limitations under the License.\n\n'use strict';\n\nvar express = require('express');\nvar debug = require('debug')('raneto');\n\n\/\/ [START setup]\nvar passport = require('passport');\nvar GoogleStrategy = require('passport-google-oauth20').Strategy;\n\nfunction extractProfile (profile) {\nvar imageUrl = '';\nif (profile.photos &amp;amp;&amp;amp; profile.photos.length) {\nimageUrl = profile.photos[0].value;\n}\nreturn {\nid: profile.id,\ndisplayName: profile.displayName,\nimage: imageUrl\n};\n}\n\n\/\/ [START middleware]\n\/\/ Middleware that requires the user to be logged in. If the user is not logged\n\/\/ in, it will redirect the user to authorize the application and then return\n\/\/ them to the original URL they requested.\nfunction authRequired (req, res, next) {\nif (!req.user) {\nreq.session.oauth2return = req.originalUrl;\nreturn res.redirect('\/login');\n}\nnext();\n}\n\n\/\/ Middleware that exposes the user's profile as well as login\/logout URLs to\n\/\/ any templates. These are available as `profile`, `login`, and `logout`.\nfunction addTemplateVariables (req, res, next) {\nres.locals.profile = req.user;\nres.locals.login = '\/auth\/login?return=' +\nencodeURIComponent(req.originalUrl);\nres.locals.logout = '\/auth\/logout?return=' +\nencodeURIComponent(req.originalUrl);\nnext();\n}\n\/\/ [END middleware]\n\nfunction router(config) {\n\/\/ Configure the Google strategy for use by Passport.js.\n\/\/\n\/\/ OAuth 2-based strategies require a `verify` function which receives the\n\/\/ credential (`accessToken`) for accessing the Google API on the user's behalf,\n\/\/ along with the user's profile. The function must invoke `cb` with a user\n\/\/ object, which will be set at `req.user` in route handlers after\n\/\/ authentication.\npassport.use(new GoogleStrategy({\nclientID: config.oauth2.client_id,\nclientSecret: config.oauth2.client_secret,\ncallbackURL: config.oauth2.callback,\nhostedDomain: config.hostedDomain || '',\naccessType: 'offline',\n\n}, function (accessToken, refreshToken, profile, cb) {\n\/\/ Extract the minimal profile information we need from the profile object\n\/\/ provided by Google\ncb(null, extractProfile(profile));\n}));\n\npassport.serializeUser(function (user, cb) {\ncb(null, user);\n});\npassport.deserializeUser(function (obj, cb) {\ncb(null, obj);\n});\n\/\/ [END setup]\n\nvar router = express.Router();\n\n\/\/ Begins the authorization flow. The user will be redirected to Google where\n\/\/ they can authorize the application to have access to their basic profile\n\/\/ information. Upon approval the user is redirected to `\/auth\/google\/callback`.\n\/\/ If the `return` query parameter is specified when sending a user to this URL\n\/\/ then they will be redirected to that URL when the flow is finished.\n\/\/ [START authorize]\nrouter.get(\n\/\/ Login url\n'\/auth\/login',\n\n\/\/ Save the url of the user's current page so the app can redirect back to\n\/\/ it after authorization\nfunction (req, res, next) {\nif (req.query.return) {\nreq.session.oauth2return = req.query.return;\n}\nnext();\n},\n\n\/\/ Start OAuth 2 flow using Passport.js\npassport.authenticate('google', { scope: ['email', 'profile'] })\n);\n\/\/ [END authorize]\n\n\/\/ [START callback]\nrouter.get(\n\/\/ OAuth 2 callback url. Use this url to configure your OAuth client in the\n\/\/ Google Developers console\n'\/auth\/google\/callback',\n\n\/\/ Finish OAuth 2 flow using Passport.js\npassport.authenticate('google'),\n\n\/\/ Redirect back to the original page, if any\nfunction (req, res) {\nreq.session.loggedIn = true;\nvar redirect = req.session.oauth2return || '\/';\ndelete req.session.oauth2return;\nres.redirect(redirect);\n}\n);\n\/\/ [END callback]\n\n\/\/ Deletes the user's credentials and profile from the session.\n\/\/ This does not revoke any active tokens.\nrouter.get('\/auth\/logout', function (req, res) {\nreq.session.loggedIn = false;\nreq.logout();\nres.redirect('\/login');\n});\nreturn router;\n}\n\nmodule.exports = {\nextractProfile: extractProfile,\nrouter: router,\nrequired: authRequired,\ntemplate: addTemplateVariables\n};\n<\/code><\/pre>\n<p>This is a changed file based on the <a href=\"https:\/\/raw.githubusercontent.com\/GoogleCloudPlatform\/nodejs-getting-started\/master\/4-auth\/lib\/oauth2.js\">Google Node.js official example<\/a> file. Notable differences are in Google strategy settings which basically load settings from our settings config:<\/p>\n<pre><code>clientID: config.oauth2.client_id,\nclientSecret: config.oauth2.client_secret,\ncallbackURL: config.oauth2.callback,\nhostedDomain: config.hostedDomain || '',\n<\/code><\/pre>\n<p>We&#8217;ll define these settings the <code>config.default.js<\/code> file now.<\/p>\n<h3>Editing the <code>example\/config.default.js<\/code> file<\/h3>\n<p>Change\/add the following settings:<\/p>\n<pre><code>allow_editing : true,\nauthentication : true,\ngoogleoauth: true,\noauth2 : {\nclient_id: 'GOOGLE_CLIENT_ID',\nclient_secret: 'GOOGLE_CLIENT_SECRET',\ncallback: 'http:\/\/localhost:3000\/auth\/google\/callback',\nhostedDomain: 'google.com'\n},\nsecret: 'someCoolSecretRightHere',\n<\/code><\/pre>\n<h3>Google OAuth2 Credentials<\/h3>\n<p>Oauth2 settings (<code>GOOGLE_CLIENT_ID<\/code> and <code>GOOGLE_CLIENT_SECRET<\/code>) can be found in your <code>Google Cloud Console-&amp;gt;API Manager-&amp;gt;Credentials<\/code> project settings (create a project if you don&#8217;t have one yet):<\/p>\n<p><img decoding=\"async\" src=\"http:\/\/i.imgur.com\/TdkYKul.png\" alt=\"\" \/><\/p>\n<p>The <code>callback<\/code>, if testing locally, can be set as shown above (<code>http:\/\/localhost:3000\/auth\/google\/callback<\/code>). The <code>hostedDomain<\/code> option allows certain domains &#8211; for your use case you may want to set this to your domain.<\/p>\n<h4>Google+ API<\/h4>\n<p>If you get an error like:<\/p>\n<blockquote><p>\n  Access Not Configured. Google+ API has not been used in project 701766813496 before, or it is disabled. Enable it by visiting https:\/\/console.developers.google.com\/apis\/api\/plus\/overview?project=701766813496 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.\n<\/p><\/blockquote>\n<p>Make sure you enable Google+ API for your project:<\/p>\n<p><img decoding=\"async\" src=\"http:\/\/i.imgur.com\/GcymtaZ.png\" alt=\"\" \/><\/p>\n<h3>Adding Zocial CSS<\/h3>\n<p>To add support for the nice <a href=\"http:\/\/zocial.smcllns.com\/\">Zocial social buttons<\/a>, download <a href=\"https:\/\/github.com\/smcllns\/css-social-buttons\/blob\/master\/css\/zocial.css\">this file<\/a> from their Github repo to the <code>themes\/default\/public\/styles\/<\/code> folder.<\/p>\n<h3>Editing the <code>themes\/default\/templates\/layout.html<\/code> file<\/h3>\n<p>Replace the login form with:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/Hitman666\/57db551db85075bd2d9f06acfef78a51.js\"><\/script><\/p>\n<p>We added two scenarios for when we have Google OAuth enabled (<code>config.googleoauth<\/code>) and when we don&#8217;t (defaulting to the current Raneto behavior).<\/p>\n<h3>Editing the <code>themes\/default\/templates\/login.html<\/code> file<\/h3>\n<p>Add zocial reference:<\/p>\n<p>&#8220;<\/p>\n<p>Replace the whole <code>form-bottom<\/code> classed div with the following code:<\/p>\n<p><script src=\"https:\/\/gist.github.com\/Hitman666\/55b4ad360b746faf367f61b8ded0d458.js\"><\/script><\/p>\n<p>Same thing here as well. If we have Google OAuth enabled (<code>config.googleoauth<\/code>) then we show the new Google login button and hide the rest. Otherwise, we default it to the current Raneto behavior.<\/p>\n<h2>Testing<\/h2>\n<p>Congratulations, you&#8217;re done! Now, to test this locally just run the <code>npm start<\/code> from the root of your project and go to <code>http:\/\/localhost:3000<\/code> and you should see this:<\/p>\n<p><img decoding=\"async\" src=\"http:\/\/i.imgur.com\/qTTwY4z.png\" alt=\"\" \/><\/p>\n<p>After logging in, you should see something like this:<\/p>\n<p><img decoding=\"async\" src=\"http:\/\/i.imgur.com\/1YcdTou.png\" alt=\"\" \/><\/p>\n<p>Hope this helps someone!<\/p>\n<blockquote class=\"twitter-tweet\" data-width=\"550\">\n<p lang=\"en\" dir=\"ltr\"><a href=\"https:\/\/twitter.com\/hashtag\/Raneto?src=hash\">#Raneto<\/a> Google OAuth login step by step <a href=\"https:\/\/t.co\/rWnoXFl0LO\">https:\/\/t.co\/rWnoXFl0LO<\/a><\/p>\n<p>&mdash; Nikola Bre\u017enjak (@HitmanHR) <a href=\"https:\/\/twitter.com\/HitmanHR\/status\/773634022920642560\">September 7, 2016<\/a><\/p><\/blockquote>\n<p><script async src=\"\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"><\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>TL;DR Raneto allows only basic username\/password authentication, so I added Google OAuth support. This option can be turned on by setting the googleoauth option in the config.default.js file&hellip;<\/p>\n","protected":false},"author":1,"featured_media":2803,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,4],"tags":[],"class_list":["post-2802","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-codeproject","category-nodejs"],"_links":{"self":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/2802","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/comments?post=2802"}],"version-history":[{"count":8,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/2802\/revisions"}],"predecessor-version":[{"id":2811,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/posts\/2802\/revisions\/2811"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/media\/2803"}],"wp:attachment":[{"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/media?parent=2802"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/categories?post=2802"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nikola-breznjak.com\/blog\/wp-json\/wp\/v2\/tags?post=2802"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}