Google sign-in using Javascript API without triggering popup - javascript

I am using following code for users to be able to login using their Google account via Javascript API.
HTML
<a id="gp_login" href="javascript:void(0)" onclick="javascript:googleAuth()">Login using Google</a>
Javascript
function gPOnLoad(){
// G+ api loaded
document.getElementById('gp_login').style.display = 'block';
}
function googleAuth() {
gapi.auth.signIn({
callback: gPSignInCallback,
clientid: googleKey,
cookiepolicy: "single_host_origin",
requestvisibleactions: "http://schema.org/AddAction",
scope: "https://www.googleapis.com/auth/plus.login email"
})
}
function gPSignInCallback(e) {
if (e["status"]["signed_in"]) {
gapi.client.load("plus", "v1", function() {
if (e["access_token"]) {
getProfile()
} else if (e["error"]) {
console.log("There was an error: " + e["error"])
}
})
} else {
console.log("Sign-in state: " + e["error"])
}
}
function getProfile() {
var e = gapi.client.plus.people.get({
userId: "me"
});
e.execute(function(e) {
if (e.error) {
console.log(e.message);
return
} else if (e.id) {
// save profile data
}
})
}(function() {
var e = document.createElement("script");
e.type = "text/javascript";
e.async = true;
e.src = "https://apis.google.com/js/client:platform.js?onload=gPOnLoad";
var t = document.getElementsByTagName("script")[0];
t.parentNode.insertBefore(e, t)
})()
This code is working fine.
I want to use the above code (using Javascript) to login user from their Google account without triggering a popup window. Like, user clicks the login link, asked for app permissions in the same window/tab, user grants permission, user redirected back to the page where Google login link was, profile data is saved and user is logged in.

You can the use ux_mode parameter (options are 'redirect' or 'popup') and set a redirect_uri if you want to redirect to a different page.
It's also necessary to authorize the URL for the OAuth client on your google project page.
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES,
ux_mode: 'redirect',
//redirect_uri: custom url to redirect'
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
});
}

Such functionality is not provided through Google API. You should stick with gapi.auth.signIn. I know just one way to make it work, but it's very hacky.
gapi.auth.signIn opens authentication window.
Save authentication window url in your app1.
Instead of calling gapi.auth.signIn, redirect user to that url.
To redirect successful authentication back to your website, add/modify redirect_url param in the url2. Keep in mind that redirect_uri must be registered in developers console.
Example:
https://accounts.google.com/o/oauth2/auth?client_id=1234567890.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fplus.login&immediate=false&response_type=token&redirect_uri=http://example.com
This way google will redirect user back to your website. access_token is provided through GET params.
1If google changes their API this may break (since this method bypasses JS API and assumes that all those params in the url will be supported for ever).
2Redirect_url is introduced in offline access flow documentation. I don't think this param was intended to work in any other cases.
I strongly advise not to use this idea (since it bypasses JS API and uses undocumented functionality). Stick with gapi.auth.signIn.

Related

Is it possible to authenticate from SPA with Microsoft Authentication Library but without Redirect Uri?

I'm creating a Javascript single page application. It requires a user to sign in during the implicit flow in order to use Microsoft Graph API later on.
I'm using MSAL.js and trying to adapt snippets from this guide to get auth token from authRedirectCallBack
I realize that in other flows redirect uri (used after authentication) is essential to get the token and proceed with it.
However in my flow I have token processed in the Javascript callback function.
Is it possible to avoid providing redirect uri in App registration with Azure AD and/or during the code execution? I'd like not to bring the user to this redirect uri at all.
At the moment my code looks like this:
var msalConfig = {
auth: {
clientId: 'my-app-id',
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true,
forceRefresh: false
}
};
const loginRequest = {scopes: ['openid', 'profile', 'user.read']};
const msalClient = new Msal.UserAgentApplication(msalConfig);
msalClient.handleRedirectCallback(authRedirectCallBack);
singIn();
async function singIn() {
try {
msalClient.loginPopup(loginRequest).then(function (response) {
if (msalClient.getAccount()) {
console.log('logged');
}
});
} catch (err) {
console.log(err);
}
}
function authRedirectCallBack(err, response) {
if (err) {
console.log(err);
} else {
if (response.tokenType === "access_token") {
console.log('token', response);
}
}
}
A redirect URI is required, as it is the main security mechanism we use to ensure the response is not returned to an unauthorized party, as the Implicit Flow relies on redirecting iframes/popups/windows back to your application with the response in the hash of the url.
Note, that if you use loginPopup/acquireTokenPopup/acquireTokenSilent, the end user will not really "see" this redirect page, as it is visited only briefly in either a hidden iframe (for acquireTokenSilent) or popup window. As soon as MSAL.js sees that the iframe/popup has been redirected back to your application, the response is parsed and the popup/iframe is closed.
We are working on a new version of the library that will switch to the Auth Code Flow w/ PKCE, which will not use hidden iframes, however, it will still use popups/redirects.
Can you further explain why you don't want your users to visit the redirect page?

Keeping user logged in after refresh/using refresh token with Google OAuth2 in React app

I’m building a React app where a key part of the functionality is a user can sign into their Google account and then access a feed of their most recent Google Drive/Docs mentions and notifications. A user arrives at my site where I load the Google OAuth2 client with my client_id, apiKey, scope and discoveryDocs, and they can click a button to sign in. For convenience, I’d like the user to not have to re-login and re-auth with their Google account every time they use the app or the app refreshes, I’d like the login information to be saved across sessions. For this I’ll use localStorage to start but eventually integrate a database like Firebase.
After looking through the JavaScript client Google OAuth2 docs I understand how most things work - understand the data and methods stored in the GoogleUser, GoogleAuth, etc objects. I’m having a little trouble with access and refresh tokens. I recognize that you can get the authenticated user’s information through gapi.auth2.getAuthInstance().currentUser.get() and gapi.auth2.getAuthInstance().currentUser.get().getAuthResponse() returns an object with a lot of what I think I need like id_token, access_token and metadata like expires_at and token_type. I also see the grantOfflineAccess() method from which I extract response.code, but I’m not quite sure which of these tokenized strings is the right one to use and how I need to use it.
This FAQ from Google (https://developers.google.com/api-client-library/javascript/help/faq) is somewhat helpful but advises to Refresh the token by calling gapi.auth.authorize with the client ID, the scope and immediate:true as parameters., but gapi.auth.authorize is noted by Google in the client JS OAuth2 library as being incompatible with the more widely used and heavily documented api.auth2.init and signIn.
I also have a vague idea from posts like Google OAuth2 API Refresh Tokens that I need to follow server-side OAuth2 instructions and I can only get this refresh_token through a server-side call, but I’m still at a bit of a loss. I’ll caveat and say I’m more of a front end developer/designer so I'm shaky on my node and server-side skills.
TL;dr: I don't know how to keep my users who signed in via Google OAuth2 signed in after a refresh. I have an idea it's due to refresh_token and access_token and I have access to them but I don't know what to do after that, in terms of sending data to Google servers, getting information back, and setting the token information for the given user when they return.
Here's my method that calls on componentDidMount (basically when my app first loads):
loadGoogleClient = () => {
gapi.load("client:auth2", () => {
gapi.auth2.init({
'client_id': my-client-id,
'apiKey': my-key,
'scope': "https://www.googleapis.com/auth/drive.readonly",
'discoveryDocs': ['https://content.googleapis.com/discovery/v1/apis/drive/v3/rest']
})
// Listen for sign-in state changes.
console.log(`User is signed in: ${gapi.auth2.getAuthInstance().isSignedIn.get()}`);
gapi.client.load("https://content.googleapis.com/discovery/v1/apis/drive/v3/rest")
.then(() => { console.log("GAPI client loaded for API");
}, (error) => { console.error("Error loading GAPI client for API", error);
});
console.log('Init should have worked');
});
}
And here's my code that's onClick on my Signin button:
authGoogle = () => {
gapi.auth2.getAuthInstance()
.signIn({scope: "https://www.googleapis.com/auth/drive.readonly"})
.then(function() { console.log("Sign-in successful"); },
function(err) { console.error("Error signing in", err); });
}
If you are using the client lib (the gapi api) there is no need for a refresh token... Once logged in it should persist across sessions and refreshes... The issue is the code...
1) Include this in your index.html in the head section:
<script src="https://apis.google.com/js/api.js"></script>
2) Here is a component that will handle auth using the gapi lib and render a button conditionally (The code is self-explanatory but if you have a question just ask...)
import React from 'react';
class GoogleAuth extends React.Component {
state = { isSignedIn: null };
componentDidMount() {
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: '<your client id here...>',
scope: 'email', // and whatever else passed as a string...
})
.then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
this.handleAuthChange();
this.auth.isSignedIn.listen(this.handleAuthChange);
});
});
}
handleAuthChange = () => {
this.setState({ isSignedIn: this.auth.isSignedIn.get() });
};
handleSignIn = () => {
this.auth.signIn();
};
handleSignOut = () => {
this.auth.signOut();
};
renderAuthButton() {
if (this.state.isSignedIn === null) {
return null;
} else if (this.state.isSignedIn) {
return <button onClick={this.handleSignOut}>Sign Out</button>;
} else {
return <button onClick={this.handleSignIn}>Sign in with Google</button>;
}
}
render() {
return <div>{this.renderAuthButton()}</div>;
}
}
export default GoogleAuth;
Now you can simply use this component/button anywhere in your app... Meaning if you have a Navigation component simply import it there and use it as a button login / log out...

Keycloak JavaScript API to get current logged in user

We plan to use keycloak to secure a bunch of web apps, some written in Java, some in JavaScript (with React).
After the user is logged in by keycloak, each of those web apps needs to retrieve the user that is logged in and the realm/client roles that the user has.
For Java apps, we tried the keycloak Java API (request -> KeycloakSecurityContext -> getIdToken -> getPreferredUsername/getOtherClaims). They seem to work fine
For JavaScript apps, we tried the following code, but could not get Keycloak to init successfully (Note this is in web app code after the user is already authenticated by keycloak, the app is only trying to retrieve who logged in with what roles):
var kc = Keycloak({
url: 'https://135.112.123.194:8666/auth',
realm: 'oneRealm',
clientId: 'main'
});
//this does not work as it can't find the keycloak.json file under WEB-INF
//var kc = Keycloak('./keycloak.json');
kc.init().success(function () {
console.log("kc.idToken.preferred_username: " + kc.idToken.preferred_username);
alert(JSON.stringify(kc.tokenParsed));
var authenticatedUser = kc.idTokenParsed.name;
console.log(authenticatedUser);
}).error(function () {
window.location.reload();
});
I assume it would be fairly common that web apps need to retrieve current user info. Anyone knows why the above code didn't work?
Thanks.
<script src="http://localhost:8080/auth/js/keycloak.js" type="text/javascript"></script>
<script type="text/javascript">
const keycloak = Keycloak({
"realm": "yourRealm",
"auth-server-url": "http://localhost:8080/auth",
"ssl-required": "external",
"resource": "yourRealm/keep it default",
"public-client": true,
"confidential-port": 0,
"url": 'http://localhost:8080/auth',
"clientId": 'yourClientId',
"enable-cors": true
});
const loadData = () => {
console.log(keycloak.subject);
if (keycloak.idToken) {
document.location.href = "?user="+keycloak.idTokenParsed.preferred_username;
console.log('IDToken');
console.log(keycloak.idTokenParsed.preferred_username);
console.log(keycloak.idTokenParsed.email);
console.log(keycloak.idTokenParsed.name);
console.log(keycloak.idTokenParsed.given_name);
console.log(keycloak.idTokenParsed.family_name);
} else {
keycloak.loadUserProfile(function() {
console.log('Account Service');
console.log(keycloak.profile.username);
console.log(keycloak.profile.email);
console.log(keycloak.profile.firstName + ' ' + keycloak.profile.lastName);
console.log(keycloak.profile.firstName);
console.log(keycloak.profile.lastName);
}, function() {
console.log('Failed to retrieve user details. Please enable claims or account role');
});
}
};
const loadFailure = () => {
console.log('Failed to load data. Check console log');
};
const reloadData = () => {
keycloak.updateToken(10)
.success(loadData)
.error(() => {
console.log('Failed to load data. User is logged out.');
});
}
keycloak.init({ onLoad: 'login-required' }).success(reloadData);
</script>
simple javascript client authentication no frameworks.
for people who are still looking...
Your code asks the Keycloak client library to initialize, but it doesn't perform a login of the user or a check if the user is already logged in.
Please see the manual for details: http://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter
What your probably want to do:
Add check-sso to the init to check if the user is logged in and to retrieve the credentials keycloak.init({ onLoad: 'check-sso' ... }). You might even use login-required.
Make sure that you register a separate client for the front-end. While the Java backend client is of type confidential (or bearer only), the JavaScript client is of type public.
You find a very minimal example here: https://github.com/ahus1/keycloak-dropwizard-integration/blob/master/keycloak-dropwizard-bearer/src/main/resources/assets/ajax/app.js
Alternatively you can register a callback for onAuthSuccess to be notified once the user information has been retrieved.
Once you use Keycloak in the front-end, you will soon want to look in bearer tokens when calling REST resources in the backend.
You might have solved the problem by this time. I hope this answer help rest of the people in trouble.
when you use JavaScript Adopter
Below javascript should be added in of html page.
<script src="http://localhost:8080/auth/js/keycloak.js"></script>
<script>
/* If the keycloak.json file is in a different location you can specify it:
Try adding file to application first, if you fail try the another method mentioned below. Both works perfectly.
var keycloak = Keycloak('http://localhost:8080/myapp/keycloak.json'); */
/* Else you can declare constructor manually */
var keycloak = Keycloak({
url: 'http://localhost:8080/auth',
realm: 'Internal_Projects',
clientId: 'payments'
});
keycloak.init({ onLoad: 'login-required' }).then(function(authenticated) {
alert(authenticated ? 'authenticated' : 'not authenticated');
}).catch(function() {
alert('failed to initialize');
});
function logout() {
//
keycloak.logout('http://auth-server/auth/realms/Internal_Projects/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri')
//alert("Logged Out");
}
</script>
https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter Reference Link.
Note : Read the comments for 2 methods of adding json credentials.

Handling oauth2 redirect from electron (or other desktop platforms)

This is mostly a lack of understanding of oauth2 and probably not specific to electron, however I'm trying to wrap my head around how someone would handle an oauth2 redirect url from a desktop platform, like electron?
Assuming there is no webservice setup as part of the app, how would a desktop application prompt a user for credentials against a third party oauth2 service, and then authenticate them correctly?
Electron JS runs a browser instance on your localhost. Therefore, you can handle an oauth2 redirect url by supplying a callback url of https:localhost/whatever/path/you/want. Just be sure to white list it on the oauth2 app registration page for whatever service you are using.
Example:
var authWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
'node-integration': false,
'web-security': false
});
// This is just an example url - follow the guide for whatever service you are using
var authUrl = 'https://SOMEAPI.com/authorize?{client_secret}....'
authWindow.loadURL(authUrl);
authWindow.show();
// 'will-navigate' is an event emitted when the window.location changes
// newUrl should contain the tokens you need
authWindow.webContents.on('will-navigate', function (event, newUrl) {
console.log(newUrl);
// More complex code to handle tokens goes here
});
authWindow.on('closed', function() {
authWindow = null;
});
A lot of inspiration taken from this page: http://manos.im/blog/electron-oauth-with-github/
Thank you for this solution. I also noticed that the navigate events from the webContents are not reliable when no clicks on the browser window triggers the redirection to the application redirect uri. For example Github login page would never trigger this event with the redirect URI if I was already logged in in the browser window. (It was probably using some session storage).
The workaround I found was to use WebRequest instead
const { session } = require('electron');
// my application redirect uri
const redirectUri = 'http://localhost/oauth/redirect'
// Prepare to filter only the callbacks for my redirectUri
const filter = {
urls: [redirectUri + '*']
};
// intercept all the requests for that includes my redirect uri
session.defaultSession.webRequest.onBeforeRequest(filter, function (details, callback) {
const url = details.url;
// process the callback url and get any param you need
// don't forget to let the request proceed
callback({
cancel: false
});
});

OAuth 2.0 authentication silently failing in Google Blogger JavaScript API (v3)

Until recently I was using the javascript Blogger API v3 to fetch posts from a blog to build an automated index, using the gapi.client.blogger.posts.list() method.
However, it suddenly stopped working. The response now simply includes an error object with the message "We're sorry, but the requested resource could not be found.", as if the blog I'm trying to fetch info does not exist at all. I'm pretty sure my blog ID did not change during my sleep.
So I started digging around, and I found that the OAuth authentication token returned says I'm not actually logged in Google. In the status object from the token, the signed_in property is true but the google_logged_in one is false. Yet the Google login page showed up correctly upon execution and I allowed it, and the token does not have an error property. Why does it says I'm not logged in?
I'm under the assumption that Google blocked my application from making new requests due repeated use (even thought I never reached a dent of the daily quota) and now since the OAuth 2 ID authentication does not work, the API tries to use only the web browser API key, which does not work either since it's a private blog and user permission is required. But I really don't know.
I could not find a similar situation on the internet. I tried deleting both my API and OAuth keys and making new ones, deleting the project from the Google Console Developers and creating a new one under a different account, removing the application from the "allowed apps" in my account and adding it again, not using the browser API key. No game.
I would appreciate any inputs in solving this issue. And before anyone suggests: the "conventional" way of creating an automated index using the blog's feed does not work in my case, since the blog is private and has no feed.
Bellow is the returned access token (redacted a bit):
{
_aa: "1"
access_token: "[Redacted]"
client_id: "[Redacted]"
cookie_policy: undefined
expires_at: "1460999668"
expires_in: "3600"
g_user_cookie_policy: undefined
issued_at: "1460996068"
response_type: "token"
scope: "https://www.googleapis.com/auth/blogger.readonly"
state: ""
status: {
google_logged_in: false
method: "AUTO"
signed_in: true
}
token_type: "Bearer"
}
And bellow an example code of my application:
var apiKey = "myWebBrowserApiKey";
var clientId = "myOAuth2ClientID";
var blogId = "myBlogID";
var bloggerReadOnly = "https://www.googleapis.com/auth/blogger.readonly";
var fetchParam = {
blogId: blogId,
fetchBodies: false,
fetchImages: false,
maxResults: 500,
status: "live",
view: "READER"
};
var authParamIm = {
client_id: clientId,
scope: bloggerReadOnly,
immediate: true
};
var authParam = {
client_id: clientId,
scope: bloggerReadOnly,
immediate: false
};
//Executed as soon the client:blogger.js loads
function handleClientLoad() {
gapi.client.setApiKey(apiKey);
window.setTimeout(checkAuth,1);
}
//Check if the user is already authenticated for immediate access
function checkAuth() {
gapi.auth.authorize( authParamIm, handleAuthResult );
}
function handleAuthResult(authResult) {
//If the user does already have authorization, proceed with the API call
if (authResult && !authResult.error) {
makeApiCall();
}
//If he does not, shows the authorization button
else {
authorizeButton.css('visibility', 'visible');
authorizeButton.click(handleAuthClick);
}
}
//The authorization button calls this function, that shows a Google login page
function handleAuthClick(event) {
gapi.auth.authorize( authParam, handleAuthResult );
return false;
}
//Loads the Blogger API, version 3, and runs the fetchPosts() function with a callback
function makeApiCall(){
gapi.client.load('blogger', 'v3', fetchPosts);
}
function fetchPosts(){
//Creates a request to get a list of posts from the blog, using the fetch parameters
var request = gapi.client.blogger.posts.list(fetchParam);
//Execute the request and treats the response with a callback function
request.execute(function(response){
//Do Stuff
}
}

Categories

Resources