Google Calendar API wont authenticate with service account - javascript

I'm trying to code a script with the Google calendar API which allows the script to create an event in the calendar when the function gets called.
However I have struggle with the code for the authentication since the old sign-in library gets deprecated.
My aim is that users who visit my website don't have to sign in with their google account and that I can just use an OAuth2.0 client ID or service account. I think this should be possible.
For the moment I only try to list calendar events and ofc trying to authenticate.
This is the error I get:
"You have created a new client application that uses libraries for user authentication or authorization that will soon be deprecated. New clients must use the new libraries instead; existing clients must also migrate before these libraries are deprecated. See the Migration Guide for more information."
This is my code:
<!DOCTYPE html>
<html>
<head>
<title>Google Calendar API Quickstart</title>
<meta charset="utf-8" />
</head>
<body>
<p>Google Calendar API Quickstart</p>
<div id="content"></div>
<!-- Load the API Client Library -->
<script src="https://apis.google.com/js/platform.js"></script>
<script>
gapi.load("client:auth2", function() {
gapi.client
.init({
apiKey: "<MY-API-KEY",
discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"],
clientId: "<MY-CLIENT-ID>",
scope: "https://www.googleapis.com/auth/calendar"
})
.then(function() {
gapi.auth2
.getAuthInstance()
.signIn({
prompt: "consent",
client_id:
"MY-CLIENT-ID"
})
.then(function() {
// List events on the calendar
listEvents();
});
});
});
function listEvents() {
gapi.client.calendar.events
.list({
calendarId: "primary",
timeMin: new Date().toISOString(),
showDeleted: false,
singleEvents: true,
maxResults: 10,
orderBy: "startTime"
})
.then(function(response) {
var events = response.result.items;
var content = "";
if (events.length > 0) {
for (i = 0; i < events.length; i++) {
var event = events[i];
var start = event.start.dateTime || event.start.date;
content +=
"<p>" +
event.summary +
" - " +
start +
"</p>";
}
} else {
content = "<p>No upcoming events found.</p>";
}
document.getElementById("content").innerHTML = content;
});
}
</script>
</body>
</html>
I didn't found any solutions in the Google API documentation or in the migration guide. So I'm not sure what I should change.

Service account authorization is not supported by client side JavaScript. You would need to switch to a server sided version of it, lets say node.js.
Once you have done that you need to configure domain wide delegation on your google workspace account, to grant your service account access to delegate on behalf of one of the users on your google workspace domain.
to be clear service accounts do not work with standard gmail accounts, delegation can only be configured via a google workspace domain account.

Related

how to work with google calender API in Nodes js and React

my organization is creating a hotel booking website for another company. Apart from the website we are helping this company to create, They also have their rooms or spaces listed on the external website, Airbnb precisely. so they have the same spaces and hotels listed on the website we created for them and also in Airbnb. so we need to implement a way that won't allow their clients to book the same space or room at the same range of time. I.E if a client books a room at 9:45 am in the morning and will lodge only for 2 days, that room should no longer be available for the dates range the other user has booked it. So we decided to use the google calendar API as a middle man between our own website and AirbnB. if a user books any room or space in our website, on payment successful on the client at our ends here, The days the client wanna lodge should be added to the google calendar API to be exported to Airbnb and this should avoid bringing that consent screen google normally brings out to authenticate as we don't need to show anything to the client, we just want everything to be done underground. Most importantly I wanna do this using React.
I followed one node js video tutorial and he was able to create and read events successfully. he used the googleOauth2 playground API for authenticating. The API gave us a client Id, client Secret and most importantly a refresh token that would be used mainly to authenticate. This is the code below
// Require google from googleapis package.
const { google } = require('googleapis')
// Require oAuth2 from our google instance.
const { OAuth2 } = google.auth
// Create a new instance of oAuth and set our Client ID & Client Secret.
const oAuth2Client = new OAuth2(
'YOUR CLIENT ID GOES HERE',
'YOUR CLIENT SECRET GOES HERE'
)
// Call the setCredentials method on our oAuth2Client instance and set our refresh token.
oAuth2Client.setCredentials({
refresh_token: 'YOUR REFRESH TOKEN GOES HERE',
})
// Create a new calender instance.
const calendar = google.calendar({ version: 'v3', auth: oAuth2Client })
I didn't post everything because the remaining codes is just creating events and the likes. So I was following this format to make the events work here in react but I was unable to pass the authenticate block in react.
According to the documentation for javascript
It states that I have to add the script src file below to make gapi works for javascript
<script async defer type='text/Javascript' src="https://apis.google.com/js/api.js"></script>
But the problem now is how to implement the authentication using the refresh token available that was downloaded as json in react. The below code is how this was implemented using client and api key always showing windows for users to login before they can create events which in my case, I dont want
// Client ID and API key from the Developer Console
var CLIENT_ID = '<YOUR_CLIENT_ID>';
var API_KEY = '<YOUR_API_KEY>';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = "https://www.googleapis.com/auth/calendar.readonly";
/**
* Called when the signed in status changes, to update the UI
* appropriately. After a sign-in, the API is called.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
listUpcomingEvents();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
}
}
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).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;
}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}
So the below is what have done but am stuck as I dont know what to else from here. I dont even know if what am doing is right or wrong
useEffect(()=>{
gapi.load("client:auth2", () => {
gapi.client
.init({
clientId: "myClientId",
clientKey: "myClientKey",
})
.setToken({
access_token:
"myRefreshToken",
});
});
},[])
I need to get pass authentication to be able to create, read events in google calendar in REACT

Google Picker API with a valid access_token asks for sign in on first use

Firstly, I have reviewed multiple SO questions relating to similar issues and the Google Picker API docs but cannot find a solution. Links at the bottom of this post.
Goal
After using the Google auth2 JavaScript library to complete a ux_mode='popup' Sign In process for a user to obtain an access_code, I wish to open a Google Picker window using the picker JavaScript library and the obtained access_code.
Expected
After checking the access_code is still valid, when the google.picker.PickerBuilder() object is set to visible via picker.setVisible(true) the user is ALWAYS able to select files as per the configuration set for the picker object.
Actual
On a fresh browser, encountering this flow results in the Google Picker window asking the user to sign in again. If the user chooses to do so an additional popup will be triggered that automatically executes a login flow again and the user is then informed that "The API developer key is invalid."
What is truly unexpected about this is that if the user refreshes the page and repeats the exact same actions the Google Picker works exactly as expected. The only way to replicate the anomalous behaviour is to clear all the browser cache, cookies and any other site related settings.
On the JavaScript console there are the common errors of:
Failed to execute ‘postMessage’ on ‘DOMWindow’: The target origin provided (‘https://docs.google.com’) does not match the recipient window’s origin (‘http://localhost’).
Invalid X-Frame-Options header was found when loading “https://docs.google.com/picker?protocol=gadgets&origin=http%3A%2F%2Flocalhost&navHidden=true&multiselectEnabled=true&oauth_token=.............: “ALLOW-FROM http://localhost” is not a valid directive.
But otherwise, no other indication of error in the console, and the exact same errors are reported when the Picker works exactly as expected.
List of things I have confirmed
I have added the Google Picker API to the associated project in the Google developer console
I am working with a validated OAuth application configured in the OAuth Consent Screen
I have tried working with both localhost and an https:// top level domain with valid cert and registered with the Google console as verified
I have generated an API Key that is explicitly associated with the Picker API and the relevant URLs
The API key is set as the .setDeveloperKey(APIKey)
The API key does show usage in the GCP console
The clientId is correct and the appId is correct for the GCP project
I have tried with scopes of ['https://www.googleapis.com/auth/drive.file'] and with scopes of ['openid', 'email', 'profile', 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/documents']
Attempts to Resolve
I can replicate this behaviour with the bare minimum example provided in the docs Google Picker Example used as a direct cut and paste, only replacing the required credential strings.
Right before invoking the picker = new google.picker.PickerBuilder() I have validated the access_token by executing a GET fetch to https://www.googleapis.com/oauth2/v1/tokeninfo and the signin behavior still results when this returns a successful validation of the token.
I check the token using this simple function:
function checkToken(access_token) {
fetch("https://www.googleapis.com/oauth2/v1/tokeninfo", {
method: "GET",
headers: {
'Authorization': 'Bearer ' + access_token
}
}).then(response => {
if (response.status === 200) {
return response.json();
} else {
console.log('User Token Expired');
resetLoginCache();
}
}).then(data => {
console.log('User Token Still Valid');
}).catch((error) => {
console.error('User Token Check Error:', error);
})
}
The JavaScript API's are initialized with:
<meta name="google-signin-client_id" content="<clientId>">
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
function initApi() {
gapi.load('signin2:auth2:picker', () => {
window.gapi.auth2.init(
{
'client_id': clientId,
'scope': scope.join(" ")
});
});
};
In my application code I've (poorly) implemented a simple generalization of a picker builder:
// Use the Google API Loader script to load the google.Picker script.
function loadPicker( mimeTypes = null, callback = pickerCallback ) {
gapi.load('picker', {
'callback': () => {
console.log("Gonna build you a new picker...")
createPicker( mimeTypes, callback );
}
});
}
// Create and render a Picker object for searching images.
function createPicker( mimeTypes, callback ) {
let gAuthTop = gapi.auth2.getAuthInstance();
let gUser = gAuthTop.currentUser.get();
let gAuthResp = gUser.getAuthResponse(true);
console.log(gAuthResp.access_token);
checkToken(gAuthResp.access_token)
if (pickerApiLoaded && oauthToken) {
// based on MIME type:
// FOLDER => google.picker.DocsView(google.picker.ViewId.FOLDERS)
// Cannot set mimeTypes to filter view
// DOCS => google.picker.View(google.picker.ViewId.DOCS)
// Can set MIME types to match
let selectView = new google.picker.View(google.picker.ViewId.DOCS);
if (mimeTypes) {
if (mimeTypes.includes(gdriveFolderMIME)) {
selectView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
selectView.setIncludeFolders(true);
selectView.setSelectFolderEnabled(true);
selectView.setParent('root');
} else {
selectView.setMimeTypes(mimeTypes);
}
}
let picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MINE_ONLY)
.setAppId(appId)
.setMaxItems(1)
.setOAuthToken(gAuthResp.access_token)
.addView(selectView)
.setSelectableMimeTypes(mimeTypes)
.setDeveloperKey(developerKey)
.setOrigin(window.location.protocol + '//' + window.location.host)
.setCallback(callback)
.setTitle('Application Title')
.build();
console.log('Origin was set to: ', window.location.protocol + '//' + window.location.host)
picker.setVisible(true);
}
}
I've even tried to dig into the minified code loaded by the Picker but I'm not that good at JavaScript and the Firefox debugger wasn't able to help me understand what might be triggering this. However, once the "error" has been passed once, it will not appear on the same browser again for any user account and within Firefox using Private Mode will also no longer show the sign in error until all history, cookies and cache are cleared.
As proof I have done some reasonable research, similar SO questions that I have reviewed and tried working with are:
Google picker asking to sign in even after providing access token
The API developer key is invalid when viewing file in google picker
Is it possible to open google picker with access token which is fetched from server side Oauth2?
How do I use Google Picker to access files using the “drive.file” scope?
Google Picker with AccessToken not working
Google Picker API sign in
Picker API - manually set access_token
Google Picker API - how to load a picker on demand
As well as the following documentation:
G Suite Developer Picker Documentation
Google Sign-In JavaScript client reference
Cannot find any related issue on the tracker

Silent authentication for own website inside tab of custom Teams app

After two months of experimenting with Teams Authentication via adal.js and msal.js and failure, I’m close to giving up. So I really need your help.
Basically I need to “silently” authenticate the logged in Teams User for my own website (tab) inside my app that I created with App Studio. The reason for that is, so that I can use the data of the authentication token for the login of my own website.
So far I was only able to get this working with msal.js and a popup, which according to Teams developer I’ve asked is not the way to go. Understandable, since I cannot use the popup method on the Teams Client because it gets blocked.
I’ve tried this silent login method (https://github.com/OfficeDev/microsoft-teams-sample-complete-node/blob/master/src/views/tab-auth/silent.hbs) that was recommend to me.
Sadly it didn’t work. All I get is a “Renewal failed: Token renewal operation failed due to timeout” error.
Since the msal.js popup variant (Node.js Azure Quick Start Example) I used before worked in a web browser, I don’t think that the configuration of Azure App is wrong.
This is my code so far:
// onLoad="prepareForm()"
<!--- Import package for authentication information in Teams/Azure--->
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.15/js/adal.min.js" integrity="sha384-lIk8T3uMxKqXQVVfFbiw0K/Nq+kt1P3NtGt/pNexiDby2rKU6xnDY8p16gIwKqgI" crossorigin="anonymous"></script>
<script src="https://statics.teams.microsoft.com/sdk/v1.4.2/js/MicrosoftTeams.min.js" crossorigin="anonymous"></script>
<script language="JavaScript">
let config = {
clientId: "1402f497-d6e8-6740-9412-e12def41c451", // I've changed it for this stackoverflow post
redirectUri: "https://myredirect.com", // I've changed it for this stackoverflow post
cacheLocation: "localStorage",
navigateToLoginRequestUrl: false,
};
microsoftTeams.initialize()
/// START Functions for Teams
function getTeamsContext() {
microsoftTeams.getContext(function(context) {
startAuthentication(context);
});
}
function startAuthentication(teamsContext) {
if (teamsContext.loginHint) {
config.extraQueryParameters = "scope=openid+profile&login_hint=" + encodeURIComponent(teamsContext.loginHint);
} else {
config.extraQueryParameters = "scope=openid+profile";
}
let authContext = new AuthenticationContext(config);
user = authContext.getCachedUser();
if (user) {
if (user.profile.oid !== teamsContext.userObjectId) {
authContext.clearCache();
}
}
let token = authContext.getCachedToken(config.clientId);
if (token) {
console.log(token)
// Get content of token
} else {
// No token, or token is expired
authContext._renewIdToken(function (err, idToken) {
if (err) {
console.log("Renewal failed: " + err);
// Some way of logging in via Popup or similiar
} else {
console.log(idToken)
// Get content of token
}
});
}
}
/// END Functions for Teams
// initialized on page load!
function prepareForm() {
getTeamsContext();
document.InputForm.password.focus()
}
<script/>
Those are my questions:
What causes this error?
How do I authenticate the token on manipulation and is it Teams or Azure? (Does adal.js any functions for this?)
How do I login if the silent authentication fails and popups are blocked? Is there a website for authentication provided by Teams that returns a token?
Are there any working examples of the silent authentication that are not from the official Microsoft website? (I don't understand them.)

How to retrieve the Azure Active Directory logged in user information from Javascript?

I have a web app where the client side is developed in Javascript. I have already enabled Azure AD login for my app by configuring it at portal.azure.com. Then, every time when this app loads, users are required to log in, if they have not.
I would like to have some Javascript in my client side so the app knows the user name. Here is the code I have.
<script src="https://secure.aadcdn.microsoftonline-p.com/lib/1.0.11/js/adal.min.js"></script>
<script type="text/javascript">
var authContext = new AuthenticationContext({
clientId: 'xxxx-xxx-xxxx',
postLogoutRedirectUri: window.location
});
var user = authContext.getCachedUser();
if (user) {
window.alert("Signed in as " + user.userName);
} else{
window.alert("Failed to get the user information");
}
</script>
However, the variable user is always null. Can anybody help?
That seems you are using "Authentication / Authorization" feature of azure app service and the identity provide is azure ad . If you want to access the tokens from a client (like JavaScript in a browser), or if you want to get a richer set of information about the logged in user, you can also send an authenticated GET request to the /.auth/me endpoint. Javascript code below to get user claims is for your reference:
<script type="text/javascript">
$(document).ready(function ()
{
$.get("https://xxxxxx.azurewebsites.net/.auth/me", function (data, status) {
for (var key in data[0]["user_claims"]) {
var obj = data[0]["user_claims"][key];
alert(obj["typ"]); //claim type in user_claims
alert(obj["val"]) //claim value in user_claims
}
});
});
</script>
Thanks, Yan. That pretty solves my problem, only with little revision to your code. My situation is that I need to retrieve the user name first before generating the later part of my app. So, I removed the outer wrapper $(document).ready(function(){}. Correct me if I am wrong. Probably this out wrapper is telling this chunk of code to run after the entire app is loaded. Then, the final code is like this:
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script type="text/javascript">
$.get("https://xxxxx.azurewebsites.net/.auth/me", function (data) {
labeler = data[0]['user_id'];
window.alert("You logged in as " + data[0]['user_id']);
});
</script>

Okta Sign-in Widget using Okta API

I'm trying to use Okta's Sign-In Widget and the Okta API to find out what groups a user is associated with.
The Okta API which returns what groups a user is associated with but I'm only able to use this when I go to it from the admin console, so this must be based on a Okta admin session because if I don't I get this error:
{"errorCode":"E0000005","errorSummary":"Invalid session","errorLink":"E0000005","errorId":"oaeLznzzAC0QaaLJmjDEls5rA","errorCauses":[]}
How can I use the Okta Sign-In Widget and the "Get Member Groups" API resource to redirect the user based on the group they are associated with?
Rather than use the "Get Member Groups" API resource to get the groups that an Okta user is associated with, I suggest configuring the Okta Sign-In Widget to have the group claims returned to your code directly. The code below shows how to do this and check if the user is in a group named "Example".
<!DOCTYPE html>
<html>
<head>
<title>Get Groups for Okta User using the Okta Sign-In Widget</title>
<script src="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/1.7.0/js/okta-sign-in.min.js" type="text/javascript"></script>
<link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/1.7.0/css/okta-sign-in.min.css" type="text/css" rel="stylesheet">
<link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/1.7.0/css/okta-theme.css" type="text/css" rel="stylesheet">
</head>
<body>
<div id="okta-login-container"></div>
<script type="text/javascript">
var OKTA_ORG_URL = 'https://example.okta.com';
var OKTA_CLIENT_ID = '0abcdefgHIjkL12mn3oP';
var oktaSignIn = new OktaSignIn({
authParams: {
responseType: 'id_token',
responseMode: 'okta_post_message',
scopes: ['openid', 'groups']
},
clientId: OKTA_CLIENT_ID,
baseUrl: OKTA_ORG_URL
});
oktaSignIn.renderEl(
{ el: '#okta-login-container' },
function (res) {
if (res.status === 'SUCCESS') {
console.log('User successfully authenticated');
console.log(res);
if (res.claims.groups.includes('Example')) {
console.log("User in 'Example' group");
// Uncomment the line below to redirect to example.com
// window.location = "http://www.example.com";
}
}
}
);
</script>
</body>
</html>
Note: You must have the "Groups claim" in the "Sign On" section your Okta app is configured to pass along the groups you want. The configuration screen for the Groups claim is below. I've set the claim to pass along all groups that the user is assigned to. You'll likely want to configure it to only pass through the groups that you care about.

Categories