Chrome Auto Login - Deprecation Passing 'PasswordCredential' objects into 'fetch - howto solve? - javascript

Hi we have been using the following flow to trigger a custom Chrome logon event if the customer is known. Recently we have run into this warning.
(index):1259 [Deprecation] Passing 'PasswordCredential' objects into 'fetch(..., { credentials: ... })' is deprecated, and will be removed in M62, around October 2017. See https://www.chromestatus.com/features/5689327799500800 for more details and https://developers.google.com/web/updates/2017/06/credential-management-updates for migration suggestions.
We use the following to logon the customer.
My question is: 1) how can we mitigate/solve the above 'Deprecation' issue? And 2) is there a more native/standard method to ask for a confirmation to logon? (pref. in the users own browser language)
<script>
window.onload = function(e) {
var debug = false;
var _waitandaskagain = 1800;
var start = new Date()
var _askforconfirmation = false;
var cookie_name = "smartlock_cancel_cookie";
var smartlock_cancel_cookie = getCookie(cookie_name);
if (smartlock_cancel_cookie) {
return
} else {
navigator.credentials.get({
password: true,
}).then(function(cred) {
if (cred) {
if (cred.type == 'password') {
var form = new FormData();
cred.additionalData = form;
cred.additionalData.append("form_key", "SECRET");
var url = 'https://ourdomain.com/webcustomer/account/loginpostgoogle/';
if (_askforconfirmation && confirm('Logon to the website securely? Please Confirm')) {
fetch(url, {
method: 'POST',
credentials: cred
}).then(function(response) {
if (response.status == 202) {
if (debug) {
console.log('Login success; reloading now');
return;
}
navigator.credentials.store(cred);
window.location.reload();
}
if (debug) {
console.log('Server status: ' + response.status);
}
return;
}).catch(function(err) {
console.log('Smartlock Ajax error:' + err.message);
}).then(always, always);
} else {
expiry.setDate(start.getDate() + (_waitandaskagain));
document.cookie = cookie_name+"=true; expires=" + expiry.toGMTString() + "; path=/";
return;
}
}
} else if (typeof cred === "undefined") {
var end = new Date();
if (+end >= (+start + 100)) {
if (debug) {
console.log('Manual cancel detected - too slow');
}
expiry.setDate(start.getDate() + (_waitandaskagain));
document.cookie = cookie_name+"=true; expires=" + expiry.toGMTString() + "; path=/";
}
if (debug) {
console.log('Credentials undefined');
}
return;
}
});
}
}

1) Do not send credential object in the fetch, instead manually append to the form (or JSON), object things from the credential Object
fetch(url, {
method: 'POST',
credentials: cred // NO
}
// example with JSON
fetch(url, {
method: 'POST',
body: JSON.stringify({
pass: cred.password,
id: cred.id,
"form_key": "SECRET"
})
}
2) There is no standard in this regard, UX is completly up to you

Related

ASP.NET REST POST - JavaScript (jQuery) Send Body in POST

I have a REST call that is working fine when I call it from a C# app, but I can't make my JavaScript page send the content in the body of the POST. This is the REST controller. Note the JavaScript below works fine if I remove the "FromBody" attribute from the call.
[Route("api/[controller]")]
public class AuthenticateController : Controller
{
[HttpPost]
public ActionResult Post([FromBody] CredentialsModel credentialsModel)
{
var authenticationModel = new AuthenticationModel { IsSuccess = false };
if (credentialsModel != null && !string.IsNullOrEmpty(credentialsModel.Username) && !string.IsNullOrEmpty(credentialsModel.Password))
{
authenticationModel = SecurityBusinessLayer.IsValidUser(credentialsModel.Username, credentialsModel.Password);
}
var json = JsonConvert.SerializeObject(authenticationModel, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize });
return Content(json);
}
}
This is the JavaScript using JQuery:
function authenticate(username, password)
{
//Get the authenticate api url
var uriHref = window.location.href;
var lastIndexOfSlash = uriHref.lastIndexOf('/');
var apiPath = uriHref.substring(0, lastIndexOfSlash) + "/api";
var encodedUri = encodeURI(apiPath + "/authenticate/");
var credentials = {};
credentials["Username"] = username;
credentials["Password"] = password;
//Post the username and password to the server
$.post(encodedUri, credentials, function (data)
{
//Parse the returned data (should match Adapt.Data.Generic.AuthenticationModel)
var response = JSON.parse(data);
if (response.IsSuccess)
{
//Ensure the token will expire
var expiryDate = new Date();
expiryDate = new Date(expiryDate.setTime(expiryDate.getTime() + 86400000));
//Set the auth token cookie
var cookieString = "authToken=" + response.Authtoken + "; expires=" + expiryDate.toUTCString() + ";path=/";
document.cookie = cookieString;
//Goto the xivic app page
window.location = "Index.html";
}
else
{
//Failed to log in, show error message
$("#badLoginMessage").css("visibility", "visible");
}
});
}
When you remove [FromBody, you have to post Json object instead of array]
$.ajax({
url: encodedUri,
type: 'POST',
data: {
Username: jsonString,Password:password
},
success: function (data) {
if (data.Success == true) {
}
else
{
}
},
error: function () {
},
complete: function () {
}
});
This is the working code based on #LeTungAnh, and #ibubi's code. I can't help but think that $post would still be a better method though. The reason $post was not working was that it was not sending a content type of application/json which is what ASP.NET Core requires.
function authenticate(username, password) {
//Get the authenticate api url
var uriHref = window.location.href;
var lastIndexOfSlash = uriHref.lastIndexOf('/');
var apiPath = uriHref.substring(0, lastIndexOfSlash) + "/api";
var encodedUri = encodeURI(apiPath + "/authenticate/");
var credentials = {};
credentials["Username"] = username;
credentials["Password"] = password;
var credentialsJson = JSON.stringify(credentials);
$.ajax({
url: encodedUri,
type: 'POST',
data: credentialsJson,
contentType: 'application/json',
success: function (responseJson) {
var authenticationObject = JSON.parse(responseJson)
if (authenticationObject.IsSuccess == true) {
//Ensure the token will expire
var expiryDate = new Date();
expiryDate = new Date(expiryDate.setTime(expiryDate.getTime() + 86400000));
//Set the auth token cookie
var cookieString = "authToken=" + authenticationObject.Authtoken + "; expires=" + expiryDate.toUTCString() + ";path=/";
document.cookie = cookieString;
//Goto the xivic app page
window.location = "Index.html";
}
else {
//Failed to log in, show error message
$("#badLoginMessage").css("visibility", "visible");
}
},
error: function () {
//Failed to log in, show error message
$("#badLoginMessage").css("visibility", "visible");
},
complete: function () {
}
});
}

How can I check if ECC Key and Public Key are being passed through Js file?

I'm having a problem locating where my problem is. I'm using PubNub for realtime messaging and the example project is using ECC (elliptical curve cryptography) encrypted messaging.
Every thing in the file works(animation, channel presence, etc.) except for sending the message. Every time I hit send I get this message:
Here is my chat-app.js file:
(function() {
if (typeof(user) === 'undefined') {
return;
}
var output = document.getElementById('output'),
input = document.getElementById('input'),
picture = document.getElementById('picture'),
presence = document.getElementById('presence'),
action = document.getElementById('action'),
send = document.getElementById('send');
var channel = 'fun';
var keysCache = {};
var pubnub = PUBNUB.init({
subscribe_key: 'sub-c-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
publish_key: 'pub-c-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
uuid: user.name,
auth_key: user.gtoken,
ssl: true
});
function displayOutput(message) {
if(!message) return;
if(typeof(message.text) === 'undefined') return;
var html = '';
if ('userid' in message && message.userid in keysCache) {
var signature = message.signature;
delete message.signature;
var result = ecc.verify(keysCache[message.userid].publicKey, signature, JSON.stringify(message));
if(result) {
html = '<p><img src="'+ keysCache[message.userid].picture +'" class="avatar"><strong>' + keysCache[message.userid].name + '</strong><br><span>' + message.text + '</span></p>';
} else {
html = '<p><img src="images/troll.png" class="avatar"><strong></strong><br><em>A troll tried to spoof '+ keysCache[message.userid].name +' (but failed).</em></p>';
}
output.innerHTML = html + output.innerHTML;
} else {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/user/' + message.userid, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var res = JSON.parse(xhr.responseText);
keysCache[message.userid] = {
'publicKey': res.publicKey,
'name': res.name,
'artist': res.artist,
'picture': res.picture,
'id': res.id
}
displayOutput(message);
}
};
xhr.send(null);
}
}
function getHistory() {
pubnub.history({
channel: channel,
count: 30,
callback: function(messages) {
messages[0].forEach(function(m){
displayOutput(m);
});
}
});
}
pubnub.subscribe({
channel: channel,
restore: true,
connect: getHistory,
disconnect: function(res){
console.log('disconnect called');
},
reconnect: function(res){
console.log('reconnecting to pubnub');
},
callback: function(m) {
displayOutput(m);
},
presence: function(m){
if(m.occupancy === 1) {
presence.textContent = m.occupancy + ' person online';
} else {
presence.textContent = m.occupancy + ' people online';
}
if((m.action === 'join') || (m.action === 'timeout') || (m.action === 'leave')){
var status = (m.action === 'join') ? 'joined' : 'left';
action.textContent = m.uuid + ' ' + status +' room';
action.classList.add(m.action);
action.classList.add('poof');
action.addEventListener('animationend', function(){action.className='';}, false);
}
}
});
function post() {
var safeText = input.value.replace(/\&/g, '&').replace( /</g, '<').replace(/>/g, '>');
var message = { text: safeText, userid: user.id };
var signature = ecc.sign(user.eccKey, JSON.stringify(message));
message['signature'] = signature;
pubnub.publish({
channel: channel,
message: message
});
input.value = '';
}
input.addEventListener('keyup', function(e) {
if(input.value === '') return;
(e.keyCode || e.charCode) === 13 && post();
}, false);
send.addEventListener('click', function(e) {
if(input.value === '') return;
post();
}, false);
})();
This error is coming from my ecc.js file
Uncaught TypeError: Cannot read property 'r' of undefined
However, I'm assuming the error is in the chat-app.js file because I didn't touch the ecc.js file. I got it from the project -- https://github.com/pubnub/auth-chat-demo
More info on ECC can be found here: https://github.com/jpillora/eccjs
According to your description, it looks like this line is giving you the error:
var signature = ecc.sign(user.eccKey, JSON.stringify(message));
So I am guessing you are not properly passing user.eccKey?
Do you have all the user data from oAuth (or any login auth)? or is the eccKey generated correctly upon the user sign-up?
Also, try include the non-minified version of ecc.js so you can track where the error is coming from more easily.
https://raw.githubusercontent.com/jpillora/eccjs/gh-pages/dist/0.3/ecc.js

Change URLs into files (Parse.com using Javascript CloudCode)

I need to batch change a number of image links (URL's links that exist within a class in) to image files (that Parse.com hosts).
Cloud code is (apparently) how to do it.
I've followed the documentation here but haven't had any success.
What I wanted to do is:
Take URL link from "COLUMN_1"
Make it a file
Upload file to "COLUMN_1" (overwrite existing URL). If this is dangerous- can upload it to a new column ("COLUMN_2").
Repeat for next row
This code did not work (this is my first time with JS):
imgFile.save().then(function () {
object.set("COLUMN_1", imgFile);
return object.save();
}).then(function (CLASSNAME) {
response.success("saved object");
}, function (error) {
response.error("failed to save object");
});
Can anyone recommend how to do this?
OK- this successfully works for anyone else trying.
Parse.Cloud.job("convertFiles", function(request, status) { //Cuts the rundata out of poor runs
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
// Tell the JS cloud code to keep a log of where it's upto. Manually create one row (in class "debugclass") to get an object Id
Parse.Cloud.useMasterKey();
var Debug = Parse.Object.extend("debugclass");
var queryForDebugObj = new Parse.Query(Debug);
queryForDebugObj.equalTo("objectId", "KbwwDV2S57");
// Query for all users
// var queryForSublist = new Parse.Query(Parse.Object.extend("gentest"));
queryForDebugObj.find({
success: function(results) {
var debugObj = results[0];
var processCallback = function(res) {
var entry = res[0];
var debugObj = results[0];
debugObj.set("LastObject", entry.id);
debugObj.save();
Parse.Cloud.httpRequest({
url: entry.get("smallImage2"),
method: "GET",
success: function(httpImgFile)
{
console.log("httpImgFile: " + String(httpImgFile.buffer));
var imgFile = new Parse.File("picture.jpg", {base64: httpImgFile.buffer.toString('base64')});
imgFile.save().then(function () {
console.log("2");
entry.set("smallImage1", imgFile);
entry.save(null, {
success: function(unused) {
debugObj.increment("itemDone");
sleep(20);
res.shift();
if (res.length === 0) {
process(entry.id);
return;
}
else {
processCallback(res);
}
},
error: function(unused, error) {
response.error("failed to save entry");
}
});
});
},
error: function(httpResponse)
{
console.log("unsuccessful http request");
response.error(responseString);
}
});
};
var process = function(skip) {{
var queryForSublist = new Parse.Query("genpants");
if (skip) {
queryForSublist.greaterThan("objectId", skip);
console.error("last object retrieved:" + skip);
}
queryForSublist.ascending("objectId");
queryForSublist.find().then(function querySuccess(res) {
processCallback(res);
}, function queryFailed(reason) {
status.error("query unsuccessful, length of result " + result.length + ", error:" + error.code + " " + error.message);
});
}};
process(debugObj.get("LastObject"));
},
error: function(error) {
status.error("xxx Uh oh, something went wrong 2:" + error + " " + error.message);
}
});
});

Node.js formidable file upload working slow on server

I am trying to send files as form data along with some fields using http post request in angular.js and receiving file in app.post in node.js. The file sending works fine on localhost. As they say formidable uploads files at 500 mb/sec speed but on server when I am trying to send a file of 5 to 10 mb it takes 40 to 80 seconds. Please check is there any problem in my implementation.
I am using nginx and pm2 on server.
Node.js code:
// route for uploading audio asynchronously
app.post('/v1/uploadAudio', function(req, res) {
var userName, useravatar, hasfile, ismusicfile, isType, showMe, DWimgsrc, DWid, msgtime;
var imgdatetimenow = Date.now();
var form = new formidable.IncomingForm({
uploadDir: __dirname + '/public/app/upload/music',
keepExtensions: true
});
form.on('end', function() {
res.end();
});
form.parse(req, function(err, fields, files) {
console.log("files : ", files);
console.log("fields : ", fields);
var data = {
username: fields.username,
userAvatar: fields.userAvatar,
repeatMsg: true,
hasFile: fields.hasFile,
isMusicFile: fields.isMusicFile,
istype: fields.istype,
showme: fields.showme,
dwimgsrc: fields.dwimgsrc,
dwid: fields.dwid,
serverfilename: baseName(files.file.path),
msgTime: fields.msgTime,
filename: files.file.name,
size: bytesToSize(files.file.size)
};
var audio_file = {
dwid: fields.dwid,
filename: files.file.name,
filetype: fields.istype,
serverfilename: baseName(files.file.path),
serverfilepath: files.file.path,
expirytime: imgdatetimenow + (120000)
};
files_array.push(audio_file);
ios.sockets.emit('new message music', data);
});
});
AngularJS code:
// =========================================== Audio Sending Code =====================
$scope.$watch('musicFiles', function() {
$scope.sendAudio($scope.musicFiles);
});
// opens the sent music file on music_icon click on new window
$scope.openClickMusic = function(msg) {
$http.post($rootScope.baseUrl + "/v1/getfile", msg).success(function(response) {
if (!response.isExpired) {
window.open($rootScope.baseUrl + '/' + response.serverfilename, "_blank");
} else {
var html = '<p id="alert">' + response.expmsg + '</p>';
if ($(".chat-box").has("p").length < 1) {
$(html).hide().prependTo(".chat-box").fadeIn(1500);
$('#alert').delay(1000).fadeOut('slow', function() {
$('#alert').remove();
});
}
}
});
}
// recieving new music message
$socket.on("new message music", function(data) {
if (data.username == $rootScope.username) {
data.ownMsg = true;
data.dwimgsrc = "app/images/spin.gif";
} else {
data.ownMsg = false;
}
if ((data.username == $rootScope.username) && data.repeatMsg) {
checkMessegesMusic(data);
} else {
$scope.messeges.push(data);
}
});
// replacing spinning wheel in sender message after music message delivered to everyone.
function checkMessegesMusic(msg) {
for (var i = ($scope.messeges.length - 1); i >= 0; i--) {
if ($scope.messeges[i].hasFile) {
if ($scope.messeges[i].istype === "music") {
if ($scope.messeges[i].dwid === msg.dwid) {
$scope.messeges[i].showme = true;
$scope.messeges[i].serverfilename = msg.serverfilename;
$scope.messeges[i].filename = msg.filename;
$scope.messeges[i].size = msg.size;
$scope.messeges[i].dwimgsrc = "app/images/musicplay_icon.png";
break;
}
}
}
};
}
// download music file if it exists on server else return error message
$scope.downloadMusic = function(ev, elem) {
var search_id = elem.id;
for (var i = ($scope.messeges.length - 1); i >= 0; i--) {
if ($scope.messeges[i].hasFile) {
if ($scope.messeges[i].istype === "music") {
if ($scope.messeges[i].dwid === search_id) {
$http.post($rootScope.baseUrl + "/v1/getfile", $scope.messeges[i]).success(function(response) {
if (!response.isExpired) {
var linkID = "#" + search_id + "A";
$(linkID).find('i').click();
return true;
} else {
var html = '<p id="alert">' + response.expmsg + '</p>';
if ($(".chat-box").has("p").length < 1) {
$(html).hide().prependTo(".chat-box").fadeIn(1500);
$('#alert').delay(1000).fadeOut('slow', function() {
$('#alert').remove();
});
}
return false;
}
});
break;
}
}
}
};
}
// validate file type to 'music file' function
$scope.validateMP3 = function(file) {
if (file.type == "audio/mp3" || file.type == "audio/mpeg") {
return true;
} else {
var html = '<p id="alert">Select MP3.</p>';
if ($(".chat-box").has("p").length < 1) {
$(html).hide().prependTo(".chat-box").fadeIn(1500);
$('#alert').delay(1000).fadeOut('slow', function() {
$('#alert').remove();
});
}
return false;
}
}
// sending new 'music file' function
$scope.sendAudio = function(files) {
if (files && files.length) {
$scope.isFileSelected = true;
for (var i = 0; i < files.length; i++) {
var file = files[i];
var dateString = formatAMPM(new Date());
var DWid = $rootScope.username + "dwid" + Date.now();
var audio = {
username: $rootScope.username,
userAvatar: $rootScope.userAvatar,
hasFile: $scope.isFileSelected,
isMusicFile: true,
istype: "music",
showme: false,
dwimgsrc: "app/images/musicplay_icon.png",
dwid: DWid,
msgTime: dateString
}
$socket.emit('send-message', audio, function(data) { // sending new image message via socket
});
var fd = new FormData();
fd.append('file', file);
fd.append('username', $rootScope.username);
fd.append('userAvatar', $rootScope.userAvatar);
fd.append('hasFile', $scope.isFileSelected);
fd.append('isMusicFile', true);
fd.append('istype', "music");
fd.append('showme', false);
fd.append('dwimgsrc', "app/images/musicplay_icon.png");
fd.append('dwid', DWid);
fd.append('msgTime', dateString);
fd.append('filename', file.name);
$http.post('/v1/uploadAudio', fd, {
transformRequest: angular.identity,
headers: {
'Content-Type': undefined
}
}).then(function(response) {
// console.log(response);
});
}
}
};
I've used Formidable on a couple of side projects, and when uploading to localhost, I do see the 500mb/sec quoted capability (depending on physical hardware of the computer).
However, when uploading a file over the internet, you are subject to the bandwidth limitations of your ISP upload speed as well as the download speed of your server.
You report that a 10MB file takes ~80 seconds to upload to the server. That's about 125KBps (or around 1 megabit/second) which seems fairly reasonable for a home/office ISP upload speed (depending on region of the world).
A good way to eliminate your home/office network performance from the troubleshooting equation would be to write a node.js script that uploads a file several times and calculates an average speed. Run that test file on your local computer, then try again from a different server in the cloud.

Error: invalid_client The OAuth client was not found for YouTube Analytics App Script Add on

var AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; //step 1. we can actually start directly here if that is necessary
var TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; //step 2. after we get the callback, go get token
var CLIENT_ID = ScriptProperties.getProperty('787853180530-2792spp2fgs0j7tsc2jph2ur6297tmft.apps.googleusercontent.com');
var CLIENT_SECRET = ScriptProperties.getProperty('V9mZYdRwCgCDsWDF6X2Aju0H');
//PUT YOUR URL HERE -
var REDIRECT_URL = 'https://script.google.com/macros/s/AKfycbyFabJD1uUo3NNXAVVlFVapCRnJw7dJKBmCF3X9nzhgPmxZbRM/exec';
var oauthTokenPropertyName = 'GOOGLE_OAUTH_ACCESS_TOKEN';
var oauthTokenExpiresPropertyName = 'GOOGLE_OAUTH_ACCESS_TOKEN_EXPIRES';
var refreshTokenPropertyName = 'GOOGLE_OAUTH_REFRESH_TOKEN';
function getURLForAuthorization() {
return AUTHORIZE_URL + '? response_type=code&client_id=' + CLIENT_ID + '&redirect_uri=' + REDIRECT_URL +
'&scope=https://www.googleapis.com/auth/yt-analytics.readonly&approval_prompt=force&access_type=offline&state=/profile';
}
function getAndStoreAccessToken(code) {
var parameters = {
method: 'post',
payload: 'client_id=' + CLIENT_ID + '&client_secret=' + CLIENT_SECRET + '&grant_type=authorization_code&redirect_uri=' + REDIRECT_URL + '&code=' + code
};
var response = UrlFetchApp.fetch(TOKEN_URL, parameters).getContentText();
storeOAuthValues_(response);
}
function getUrlFetchOptions() {
var token = UserProperties.getProperty(oauthTokenPropertyName);
return {
"contentType": "application/json",
"headers": {
"Authorization": "Bearer " + token,
"Accept": "application/json"
}
};
}
function attemptTokenRefresh_() {
var refreshToken = UserProperties.getProperty(refreshTokenPropertyName);
if (!refreshToken) {
Logger.log('No refresh token available to refresh with ' + refreshTokenPropertyName);
return false;
}
var requestData = {
method: 'post',
payload: {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
grant_type: 'refresh_token'
}
};
Logger.log('Attempting token refresh');
var response = UrlFetchApp.fetch(TOKEN_URL, requestData).getContentText();
storeOAuthValues_(response);
return true;
}
function storeOAuthValues_(response) {
var tokenResponse = JSON.parse(response);
var accessToken = tokenResponse.access_token;
// expires_in is in seconds and Date.now is ms
var endMs = Date.now() + tokenResponse.expires_in * 1000;
var refreshToken = tokenResponse.refresh_token;
//store the token for later retrieval
UserProperties.setProperty(oauthTokenPropertyName, accessToken);
if (refreshToken) { //on a refresh call we wont get a new refresh token, lets not wipe prev one out
UserProperties.setProperty(refreshTokenPropertyName, refreshToken);
}
UserProperties.setProperty(oauthTokenExpiresPropertyName, endMs);
}
function isOAuthed() {
if (hasValidToken_()) {
Logger.log('Valid oauth token found');
return true;
} else {
try {
return attemptTokenRefresh_();
} catch (e) {
Logger.log('Failed to refresh token with error: ' + e);
return false;
}
}
}
function hasValidToken_() {
if (!isTokenPresent_()) {
return false;
}
return (!isTokenExpired_());
}
function isTokenExpired_() {
var expirationTimeMs = UserProperties.getProperty(oauthTokenExpiresPropertyName);
if (!expirationTimeMs) {
return true;
}
expirationTimeMs = Number(expirationTimeMs);
var threshold = Date.now() + 30000;
return (expirationTimeMs < threshold);
}
function isTokenPresent_() {
var token = UserProperties.getProperty(oauthTokenPropertyName);
if (!token) { //if its empty or undefined
return false;
}
return true;
}
google developer https://console.developers.google.com/project -> project name select -> APIs&Auth -> credential
enter mail adres

Categories

Resources