How to handle google login for a widget - javascript

I'm in the process of creating a widget. The part that escapes me is implementing social media authentication.
The widget is done through embedded javascript, and takes over a <div> on the page rather than utilizing an iframe.
The problem that I am having is that I would like to avoid redirects. Not because I don't want to handle them, but because I want to make the widget as friendly as possible. The basic google signin button creates a popup which handles the authentication client side. It is absolutely perfect for my use case, except that it requires a client_id:
<script>
window.googleStart = function() {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init({
client_id: '639034356632-fgcp4bigtn908q2ev3qifl9vfqnsciuj.apps.googleusercontent.com',
// Scopes to request in addition to 'profile' and 'email'
//scope: 'additional_scope'
});
});
}
</script>
I might be wrong, but I believe that if I use this method while being embedded onto someone's website I could overwrite their client_id and cause conflicts, or perhaps google would just void the second init attempt and use their client_id. In either case I don't believe that's what I should be doing. Is there any way to setup client side authentication without redirection so that my widget does not interfere with the google api that the website may be using?
I have a method that uses redirection and avoids this problem, but I would really like to be able to complete the entire social authentication without moving from the page the widget is embedded on.

You could use a new window target to the gAuth.

Related

Google oauth session lost after page reload (javascript)

I recently moved from the deprecated gapi.auth2 to the new Google Identity Services, using the javascript client library, and noticed a big difference: if someone signs in, and then reloads the page, the session is lost, and has to sign in again, every time the page is loaded. This was not the case with the deprecated library.
The problem can be easily reproduced with the Calendar API example.
Is there any configuration option to keep the session persistent? Or do I need to store the access tokens somehow? I could not find anything relevant in the official docs.
UPDATE:
The migration guide states the following:
Previously, Google Sign-In helped you to manage user signed-in status using:
Callback handlers for Monitoring the user's session state.
Listeners for events and changes to signed-in status for a user's Google Account.
You are responsible for managing sign-in state and user sessions to your web app.
However there's absolutely no information on what needs to be done.
UPDATE 2
To be more specific, the actual issue is not making the session persistent. Managing the sign in state and user session is something I can solve.
The real problem is the access token used to call the Google APIs.
As mentioned in the comments, the access tokens are 1) short lived 2) are not stored anywhere, so even if not expired, they do not persist between page reloads.
Google provides the requestAccessToken method for this, however even if I specify prompt: '', it opens the sign-in popup. If I also specify the hint option with the signed in user's email address, than the popup opens, displays a loading animation briefly, and closes without user interaction. I could live with this, however this only works if triggered by a user interaction, otherwise the browser blocks the popup window, meaning that I cannot renew the token without user interaction, e.g. on page load. Any tips to solve this?
I faced all the same issues you described in your question.
In order to help:
Google 3P Authorization JavaScript Library: in this link we can check all the methods the new library has (it does not refresh token, etc..)
This doc says the library won't control the cookies to keep the state anymore.
Solution
Firstly I need to thanks #Sam O'Riil answer.
As Sam described: "you can somehow save access token and use it to speed-up things after page reload."
Given the the Google's exampe, we should call initTokenClient in order to configure the Google Auth and the requestAccessToken to popup the auth:
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
prompt: 'consent',
callback: tokenCallback
});
tokenClient.requestAccessToken({prompt: ''})
In your tokenCallback you can save the credentials you get somehow, e.g.:
const tokenCallback(credentials) => {
// save here the credentials using localStorage or cookies or whatever you want to.
}
Finally, when you restart/reload your application and you initialize the gapi.server again, you only need to get the credentials again and set token to gapi, like:
gapi.load('client', function() {
gapi.client.init({}).then(function() {
let credentials = // get your credentials from where you saved it
credentials = JSON.parse(credentials); // parse it if you got it as string
gapi.client.setToken(credentials);
... continue you app ...
}).catch(function(err) {
// do catch...
});
});
Doing it, your application will work after the reload. I know it could not be the best solution, but seeing what you have and the library offers, I think that's you can do.
p.s.: the token expires after 1 hour and there is no refresh token (using the implicit flow) so, you will have to ask the user to sign-in again.

Communicating with a web widget-Meteor, React, Node

I'm building a chat dashboard and widget with which a customer should be able to put the widget into their page. Some similar examples would be Intercom or Drift.
Currently, the "main" application is written in Meteor.js (it's front end is in React). I've written a <Widget /> component and thrown it inside a /widget directory. Inside this directory, I also have an index.jsx file, which simply contains the following:
import React from 'react';
import ......
ReactDOM.render(
<Widget/>,
document.getElementById('widget-target')
);
I then setup a webpack configuration with an entry point at index.jsx and when webpack is run spits out a bundle.js in a public directory.
This can then be included on another page by simply including a script and div:
<script src="http://localhost:3000/bundle.js" type="text/javascript"></script>
<div id="widget-target"></div>
A few questions:
What is wrong with this implementation? Are their any security issues to be aware of? Both the examples linked earlier seem make use of an iframe in one form or another.
What is the best way to communicate with my main meteor application? A REST API? Emit events with Socket.io? The widget is a chat widget, so I need to send messages back and forth.
How can I implement some sort of unique identifier/user auth for the user and the widget? Currently, the widget is precompiled.
1 What is wrong with this implementation? Are their any security issues to be aware of? Both the examples linked earlier seem make use of an iframe in one form or another.
As #JeremyK mentioned, you're safer within an iFrame. That being said, there's a middle route that many third parties (Facebook, GA, ...) are using, including Intercom:
ask users to integrate your bundled code within their webpage. It's then up to you to ensure you're not introducing a security vulnerability on their site. This code will do two things:
take care of setting up an iframe, where the main part of your service is going to happen. You can position it, style it etc. This ensure that all the logic happening in the iframe is safe and you're not exposed.
expose some API between your customer webpage and your iframe, using window messaging.
the main code (the iframe code) is then loaded by this first script asynchronously, and not included in it.
For instance Intercom ask customers to include some script on their page: https://developers.intercom.com/docs/single-page-app#section-step-1-include-intercom-js-library that's pretty small (https://js.intercomcdn.com/shim.d97a38b5.js). This loads extra code that sets the iFrame and expose their API that will make it easy to interact with the iFrame, like closing it, setting user properties etc.
2 What is the best way to communicate with my main meteor application? A REST API? Emit events with Socket.io? The widget is a chat widget, so I need to send messages back and forth.
You've three options:
Build your widget as an entire Meteor app. This will increase the size of the code that needs to be loaded. In exchange for the extra code, you can communicate with your backend through the Meteor API, like Meteor.call, get the reactivity of all data (for instance if you send a response to a user through your main Meteor application, the response would pop up on the client with no work to do as long as they are on the same database (no need to be on the same server)), and the optimistic UI. In short you've all what Meteor offers here, and it's probably going to be easier to integrate with your existing backend that I assume is Meteor.
Don't include Meteor. Since you're building a chat app, you'll probably need socket.io over a traditional REST API. For sure you can do a mix of both
Use Meteor DDP. (it's kind of like socket.io, but for Meteor. Meteor app use that for all requests to the server) This will include less things that the full Meteor and probably be easier to integrate to your Meteor backend than a REST API / socket.io, and will be some extra work over the full Meteor.
3 How can I implement some sort of unique identifier/user auth for the user and the widget?
This part should probably do some work on the customer website (vs in your iframe) so that you can set cookies on his page, and send that data to your iframe that's gonna talk to your server and identify the user. Wether you use artwells:accounts-guest (that's based on meteor:accounts-base) is going to depend on wether you decide to include Meteor in your iframe.
If you don't have Meteor in your iframe, you can do something like:
handle user creation yourself, by simply doing on your server
.
const token = createToken();
Users.insert({ tokens: [token] });
// send the token back to your iframe
// and set is as a cookie on your customer website
then for each call to your server, on your iframe:
.
let token;
const makeRequest = async (request) => {
token = token || getCookieFromCustomerWebsite();
// pass the token to your HTTP / socket.io / ... request.
// in the header of whatever
return await callServer(token, request);
};
in the server have a middleware that sets the user. Mine looks like:
.
const loginAs = (userId, cb) => {
DDP._CurrentInvocation.withValue(new DDPCommon.MethodInvocation({
isSimulation: false,
userId,
}), cb);
};
// my middleware that run on all API requests for a non Meteor client
export const identifyUserIfPossible = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return next();
}
const user = Users.findOne({ tokens: token });
if (!user) {
return next();
}
loginAs(user._id, () => {
next();
// Now Meteor.userId() === user._id from all calls made on that request
// So you can do Meteor.call('someMethod') as you'd do on a full Meteor stack
});
};
Asking your customers to embed your code like this doesn't follow the principles of Security by Design.
From their point of view, you are asking them to embed your prebundled code into their website, exposing their site up to any hidden security risks (inadvertent or deliberately malicious) that exist in your code which would have unrestricted access to their website's DOM, localstorage, etc.
This is why using an iframe is the prefered method to embed third party content in a website, as that content is sandboxed from the rest of it's host site.
Further, following the security principle of 'Least Privilege' they (with your guidance/examples) can set the sandbox attribute on the iframe, and explicitly lockdown via a whitelist the privileges the widget will have.
Loading your widget in an iframe will also give you more flexibility in how it communicates with your servers. This could now be a normal meteor client, using meteor's ddp to communicate with your servers. Your other suggestions are also possible.
User auth/identification depends on the details of your system. This could range from using Meteor Accounts which would give you either password or social auth solutions. Or you could try an anonymous accounts solution such as artwells:accounts-guest.
html5rocks article on sandboxed-iframes

Okta Widget NOT Rendering

Currently, using 1.6.0 of the scripts and CSS files. I have the widget integrated in my Angular 1.5.x app. I have been experiencing issues receiving 404 when trying to render the OKTA Widget as its GET request to:
https://*.oktapreview.com/api/v1/sessions/me
Which means that the widget gets rendered once and I am able to log in, but once I log out, I am not able to re-render the widget without having to refresh the browser first.
Has anyone found and resolved this issue?
I think this might be related to this Stack Overflow issue. Can you check your privacy settings?
The reason for this issue is because the widget exchanges the sessionToken (which you get after logging in) with an id_token via a hidden iframe, at which time the session cookie is also set. But, since this is in an <iframe>, it's considered a third-party cookie.
There is currently no good way around this when your browser has third-party cookies disabled without redirecting to Okta to set the session cookie. A new version (1.8.0) of the widget is going to be released this week will make this easier (See this commmit which addresses this issue). With this new version, passing in authParams.display = 'page' will perform the call to /authorize via a redirect rather than through the hidden iframe.
Now that 1.8.0 is available via Okta's CDN, here is a sample of how to use the authParams.display = 'page' setting described above:
<script>
var config = {
baseUrl: 'https://YOUR-OKTA-ORG-HERE.okta.com',
clientId: 'YOUR-OKTA-CLIENT-ID-HERE',
redirectUri: 'YOUR-REDIRECT-URL-HERE',
authParams: {
responseType: 'code',
display: 'page',
scopes: ['openid', 'email', 'profile'],
}
};
var oktaSignIn = new OktaSignIn(config);
oktaSignIn.renderEl({ el: '#app-container' }, () => {});
</script>

Google Javascript Client library OAUTH asks for Offline Access permission, though it was never requested

My application interacts with Google with Javascript only. It asks for user profile access, email access and contacts management permissions.
Upon loading a page, the application checks if the user has already granted those permissions and obtains an access token if he had.
Here is some sample code:
var GoogleContacts = {
...
checkAuth: function(){
gapi.auth.authorize({
client_id: googleKeys.clientId,
scope: googleKeys.scopes,
immediate: true
},
jQuery.proxy(this.handleAuthResult, this)
);
},
askAuth: function(){
gapi.auth.authorize({
client_id: googleKeys.clientId,
scope: googleKeys.scopes,
immediate: false
},
jQuery.proxy(this.handleAuthResult, this)
);
}
...
}
....
function handleGoogleApiLoad(){
gapi.client.setApiKey(googleKeys.apiKey);
gapi.auth.init(function(){console.info('popup api ready')});
setTimeout(function(){GoogleContacts.checkAuth();}, 300);
}
....
$('#emailButton').click(function() {
if(!accessToken)
GoogleContacts.askAuth();
...
});
Now, if user comes for the first time, he is asked the correct permissions when he pressed the "Send email" button. When user reloads a page, the seamless permissions check returns failure and when user hits a "send email" button, we open the Google authorization popup again, and it now asks for Offline Access permission.
This seems incorrect as the JS api has no actual use for offline access.
Looks like this problem started after Google released the incremental auth feature: http://googleplusplatform.blogspot.co.il/2013/12/google-sign-in-improvements11.html
Is this a bug that will soon be fixed, or should we change the code somehow to not confuse our users with weird permission requests?
Update:
I have tried to use the plus api and gapi.auth.signIn() method but with the same result.
Apparently, this problem is scope-dependant, as when I use only the login scope, everything works as expected, but adding the Google Contacts access scope https:||www.google.com/m8/feeds/ always leads to the Offline Access request when entering page second time. Here is a fiddle to confirm this: http://jsfiddle.net/hjLM6/6/
This must be a bug and I really would like Google to deal with it soon, as it scares users away.
The immediate:false parameter in your askAuth method is the cause. The post that Abraham mentions explains the background.
The gapi.auth.authorize() method should generally be avoided in most cases now that the gapi.auth.signIn() method is available to handle programatic initiation of the authorization flow and also you should make use of the dynamic callbacks. The information on monitoring a user's session state explains how and when to get your sign-in callback function to fire and how you can use the values within the auth result object to determine if they've previously authorized your app, signed in (or out) of your app, or signed in (or out) of google.
Your checkAuth and askAuth functions would effectively be combined to check the status of the auth result object and act accordingly. Your email button click event would trigger instead gapi.auth.signIn() with the necessary parameters and scopes for your app.
I just had exactly the same issue with drive.readonly scope, for what it's worth the way I worked around it is by always calling authorize with immediate = false. This isn't that bad because when you do this for an already authorized user, Google will open the popup for a fraction of a second but then will immediately close it (apparently making use of the chance to open the popup in the browser event handler in case the user does need to authorize).
Curiously, for localhost server where I previously used immediate = true, I continue to see requests for offline access - but on the production server I haven't seen seen them so far, fingers crossed.

Google OAuth WildCard Domains

I am using the google auth but keep getting an origin mismatch. The project I am working has sub domains that are generated by the user. So for example there can be:
john.example.com
henry.example.com
larry.example.com
In my app settings I have one of my origins being http://*.example.com but I get an origin mismatch. Is there a way to solve this? Btw my code looks like this:
gapi.auth.authorize({
client_id : 'xxxxx.apps.googleusercontent.com',
scope : ['https://www.googleapis.com/auth/plus.me',
state: 'http://henry.example.com',
'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'],
immediate : false
}, function(result) {
if (result != null) {
gapi.client.load('oath2', 'v2', function() {
console.log(gapi.client);
gapi.client.oauth2.userinfo.get().execute(function(resp) {
console.log(resp);
});
});
}
});
Hooray for useful yet unnecessary workarounds (thanks for complicating yourself into a corner Google)....
I was using Google Drive using the javascript api to open up the file picker, retrieve the file info/url and then download it using curl to my server. Once I finally realized that all my wildcard domains would have to be registered, I about had a stroke.
What I do now is the following (this is my use case, cater it to yours as you need to)
On the page that you are on, create an onclick event to open up a new window in a specific domain (https://googledrive.example.com/oauth/index.php?unique_token={some unique token}).
On the new popup I did all my google drive authentication, had a button to click which opened the file picker, then retrieved at least the metadata that I needed from the file. Then I stored the token (primary key), access_token, downloadurl and filename in my database (MySQL).
Back on step one's page, I created a setTimeout() loop that would run an ajax call every second with that same unique_token to check when it had been entered in the database. Once it finds it, I kill the loop and then retrieve the contents and do with them as I will (in this case I uploaded them through a separate upload script that uses curl to fetch the file).
This is obviously not the best method for handling this, but it's better than entering each and every subdomain into googles cloud console. I bet you can probably do this with googles server side oauth libraries they use, but my use case was a little complicated and I was cranky cause I was frustrated at the past 4 days I've spent on a silly little integration with google.
Wildcard origins are not supported, same for redirect URIs.
The fact that you can register a wildcard origin is a bug.
You can use the state parameter, but be very careful with that, make sure you don't create an open redirector (an endpoint that can redirect to any arbitrary URL).

Categories

Resources