ErrorAccessDenied delete attachments with microsoft graph - javascript

I'm trying to remove attachments with Microsoft Graph. I have the following function:
getAccessToken(function(accessToken) {
if (accessToken) {
// Create a Graph client
var client = MicrosoftGraph.Client.init({
authProvider: done => {
// Just return the token
done(null, accessToken);
}
});
console.log(files);
files.forEach(function(file) {
client
.api(
"/me/messages/" +
file.msg +
"/attachments/" +
file.attachment
)
.delete((err, res) => {
if (err) {
console.log(err);
return;
} else {
console.log(res);
$("#attachment_" + file.list).remove();
}
});
});
} else {
var error = {
responseText: "Could not retrieve access token"
};
}
});
// OAUTH FUNCTIONS =============================
function buildAuthUrl() {
// Generate random values for state and nonce
sessionStorage.authState = guid();
sessionStorage.authNonce = guid();
var authParams = {
response_type: "id_token token",
client_id: appId,
redirect_uri: redirectUri,
scope: scopes,
state: sessionStorage.authState,
nonce: sessionStorage.authNonce,
response_mode: "fragment"
};
return authEndpoint + $.param(authParams);
}
function handleTokenResponse(hash) {
// If this was a silent request remove the iframe
$("#auth-iframe").remove();
// clear tokens
sessionStorage.removeItem("accessToken");
sessionStorage.removeItem("idToken");
var tokenresponse = parseHashParams(hash);
// Check that state is what we sent in sign in request
if (tokenresponse.state != sessionStorage.authState) {
sessionStorage.removeItem("authState");
sessionStorage.removeItem("authNonce");
// Report error
window.location.hash =
"#error=Invalid+state&error_description=The+state+in+the+authorization+response+did+not+match+the+expected+value.+Please+try+signing+in+again.";
return;
}
sessionStorage.authState = "";
sessionStorage.accessToken = tokenresponse.access_token;
// Get the number of seconds the token is valid for,
// Subract 5 minutes (300 sec) to account for differences in clock settings
// Convert to milliseconds
var expiresin =
(parseInt(tokenresponse.expires_in) - 300) * 1000;
var now = new Date();
var expireDate = new Date(now.getTime() + expiresin);
sessionStorage.tokenExpires = expireDate.getTime();
sessionStorage.idToken = tokenresponse.id_token;
validateIdToken(function(isValid) {
if (isValid) {
// Re-render token to handle refresh
renderTokens();
// Redirect to home page
window.location.hash = "#";
} else {
clearUserState();
// Report error
window.location.hash =
"#error=Invalid+ID+token&error_description=ID+token+failed+validation,+please+try+signing+in+again.";
}
});
}
function validateIdToken(callback) {
// Per Azure docs (and OpenID spec), we MUST validate
// the ID token before using it. However, full validation
// of the signature currently requires a server-side component
// to fetch the public signing keys from Azure. This sample will
// skip that part (technically violating the OpenID spec) and do
// minimal validation
if (
null == sessionStorage.idToken ||
sessionStorage.idToken.length <= 0
) {
callback(false);
}
// JWT is in three parts seperated by '.'
var tokenParts = sessionStorage.idToken.split(".");
if (tokenParts.length != 3) {
callback(false);
}
// Parse the token parts
var header = KJUR.jws.JWS.readSafeJSONString(
b64utoutf8(tokenParts[0])
);
var payload = KJUR.jws.JWS.readSafeJSONString(
b64utoutf8(tokenParts[1])
);
// Check the nonce
if (payload.nonce != sessionStorage.authNonce) {
sessionStorage.authNonce = "";
callback(false);
}
sessionStorage.authNonce = "";
// Check the audience
if (payload.aud != appId) {
callback(false);
}
// Check the issuer
// Should be https://login.microsoftonline.com/{tenantid}/v2.0
if (
payload.iss !==
"https://login.microsoftonline.com/" +
payload.tid +
"/v2.0"
) {
callback(false);
}
// Check the valid dates
var now = new Date();
// To allow for slight inconsistencies in system clocks, adjust by 5 minutes
var notBefore = new Date((payload.nbf - 300) * 1000);
var expires = new Date((payload.exp + 300) * 1000);
if (now < notBefore || now > expires) {
callback(false);
}
// Now that we've passed our checks, save the bits of data
// we need from the token.
sessionStorage.userDisplayName = payload.name;
sessionStorage.userSigninName =
payload.preferred_username;
// Per the docs at:
// https://azure.microsoft.com/en-us/documentation/articles/active-directory-v2-protocols-implicit/#send-the-sign-in-request
// Check if this is a consumer account so we can set domain_hint properly
sessionStorage.userDomainType =
payload.tid === "9188040d-6c67-4c5b-b112-36a304b66dad"
? "consumers"
: "organizations";
callback(true);
}
function makeSilentTokenRequest(callback) {
// Build up a hidden iframe
var iframe = $("<iframe/>");
iframe.attr("id", "auth-iframe");
iframe.attr("name", "auth-iframe");
iframe.appendTo("body");
iframe.hide();
iframe.load(function() {
callback(sessionStorage.accessToken);
});
iframe.attr(
"src",
buildAuthUrl() +
"&prompt=none&domain_hint=" +
sessionStorage.userDomainType +
"&login_hint=" +
sessionStorage.userSigninName
);
}
// Helper method to validate token and refresh
// if needed
function getAccessToken(callback) {
var now = new Date().getTime();
var isExpired =
now > parseInt(sessionStorage.tokenExpires);
// Do we have a token already?
if (sessionStorage.accessToken && !isExpired) {
// Just return what we have
if (callback) {
callback(sessionStorage.accessToken);
}
} else {
// Attempt to do a hidden iframe request
makeSilentTokenRequest(callback);
}
}
Access token
eyJ0eXAiOiJKV1QiLCJub25jZSI6IkFRQUJBQUFBQUFDNXVuYTBFVUZnVElGOEVsYXh0V2pUUEF3aGltT2hjOXAxUkdiSnVjTDcyd0pjSFdTd1lpUXNFdDdqXzgxRVd6UXhvaWRaWnVXU2d5VS1HWXRqNFFNa3JjMUNpeFFTWElNRVpYQWhSUlJXZENBQSIsImFsZyI6IlJTMjU2IiwieDV0Ijoid1VMbVlmc3FkUXVXdFZfLWh4VnRESkpaTTRRIiwia2lkIjoid1VMbVlmc3FkUXVXdFZfLWh4VnRESkpaTTRRIn0.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9iNmFjNDlmNy1iMTNlLTQyOWMtYmI4NS0wODQ4OTY5NTA2OTkvIiwiaWF0IjoxNTQ0MTkyODUxLCJuYmYiOjE1NDQxOTI4NTEsImV4cCI6MTU0NDE5Njc1MSwiYWNjdCI6MCwiYWNyIjoiMSIsImFpbyI6IjQyUmdZT0JiMFhGQjBNSHFySW5FbVU5aEc4T1p6MTllOHpIRld1S0sxbldqQzVrOFAvd0IiLCJhbXIiOlsicHdkIl0sImFwcF9kaXNwbGF5bmFtZSI6IkZvcm1wcmVzcyBjbGVhciBhdHRhY2htZW50cyIsImFwcGlkIjoiNTY0NzBlMjctZTAxNC00Zjg4LWEzY2QtZjQxODNlZjBhMjkxIiwiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJLYXJsc3NvbiIsImdpdmVuX25hbWUiOiJDaHJpc3RvcGhlciIsImlwYWRkciI6IjE1NS40LjE5NC4xMzQiLCJuYW1lIjoiQ2hyaXN0b3BoZXIgS2FybHNzb24iLCJvaWQiOiI5Y2JlMjBlYy1iMTk0LTRjZTYtOTU0Zi1hMTgzNTlmNGYzYTAiLCJwbGF0ZiI6IjMiLCJwdWlkIjoiMTAwMzAwMDA5QzI0MTI0NyIsInNjcCI6Ik1haWwuUmVhZCBvcGVuaWQgcHJvZmlsZSBVc2VyLlJlYWQgZW1haWwiLCJzaWduaW5fc3RhdGUiOlsia21zaSJdLCJzdWIiOiI5NVhONDhua08zbDJIeGYyTmdVTFo2YjA3WWpVV0lHOTRUQjA2bVNRUk9vIiwidGlkIjoiYjZhYzQ5ZjctYjEzZS00MjljLWJiODUtMDg0ODk2OTUwNjk5IiwidW5pcXVlX25hbWUiOiJjaHJpc3RvcGhlckBpdHBhcnRuZXJhYi5zZSIsInVwbiI6ImNocmlzdG9waGVyQGl0cGFydG5lcmFiLnNlIiwidXRpIjoiVmJGRW9EQ0RqMEMxVkFfSTNITWRBQSIsInZlciI6IjEuMCIsInhtc19zdCI6eyJzdWIiOiJka1FicDc3c2Y5ZmJlSnVYM1NqUml3aVhPa1FYOV9MZTF6MHp4X3Q4SU5BIn0sInhtc190Y2R0IjoxNDc4ODg0NzA1fQ.YQlKVdhXQcVwkcmbpY9Wx6ENro2DL7yH1rWwwkDZLD1inUrfbLRVb67lWKzgK9GnYP81d58Fp_2CZBw8C2E4X1eo02vog6_Qga9kVb8GF2-Ue0VP0KUv8EtRpEty_DBK7Re3iOkJR9yFSPQgf11Gf15l5O2mcEifrwny5nkRvab4_ssRt6hNf53V99uTFJ3_yKycGHPTobVbyQT5ZyDKxXRwoZVprFU70qrHGcBgo5emO8HbziYCUiQ9vGMpmtz61tE0U-c0E20FPC82i3zgLfMgmhNqmljZOpkOe85PFrxoep7fYkpZpWowCozugDW0E2A3SxBLZ_JHpci2R4irxg
files is a global variable that contains an array of objects, the data seems to be correct.
Here is a sample response I get when running the code:
{
statusCode: 403,
code: "ErrorAccessDenied",
message: "Access is denied. Check credentials and try again.",
requestId: "0739c0d9-38f2-45f7-a57d-c25dfbf92f75",
date: Fri Dec 07 2018 14:54:32 GMT+0100 (centraleuropeisk normaltid), …
}
API URL
me/messages/{id}/attachments/{id}
At https://apps.dev.microsoft.com/#/application/ I have given the application Mail.ReadWrite and Mail.ReadWrite.Shared access, though it only mentions read when signing up for the application which I find odd.
It looks like my app doesn't have the correct access. Is there something else I have to do that I have missed? Is my client query incorrect?

You need to check your scopes again. The Access Token you provided only includes the following scopes: Mail.Read openid profile User.Read email.
The code sample you provided references a scopes variable but it isn't clear what you've set this to:
var authParams = {
response_type: "id_token token",
client_id: appId,
redirect_uri: redirectUri,
scope: scopes,
state: sessionStorage.authState,
nonce: sessionStorage.authNonce,
response_mode: "fragment"
};
The value of this property should be something along the lines of openid profile email Mail.ReadWrite Mail.ReadWrite.Shared.

Related

Simple MSAL Login/Authentication in JavaScript

I'm trying to do a simple login to Azure AD using the MSAL for JavaScript v2.0 library. We want users to be able to authenticate into our site with their work Microsoft accounts. All I need to do is be able to authenticate/login the user via Microsoft, and if they can login via their work Microsoft account, then they're granted access to our site.
I'm using the Javascript library and have followed the code from the Github page and while the login prompt is coming up, afterwards I have no idea how to check if the user is signed in.
Here's the code I'm using, which is basically what's in the sample code from Github:
<script type="text/javascript" src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js"></script>
<script type="text/javascript">
const msalConfig = {
auth: {
clientId: "[ClientID goes here]",
authority: "https://login.microsoftonline.com/[tenant ID]",
knownAuthorities: ["login.microsoftonline.com"],
protocolMode: "OIDC",
redirectUri: "[page on our site that doesn't have MSAL auth, listed in Azure Reply URLs]"
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: true, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case msal.LogLevel.Error:
console.error(message);
return;
case msal.LogLevel.Info:
console.info(message);
return;
case msal.LogLevel.Verbose:
console.debug(message);
return;
case msal.LogLevel.Warning:
console.warn(message);
return;
}
}
}
}
};
// Add here scopes for id token to be used at MS Identity Platform endpoints.
const loginRequest = {
scopes: ["User.Read"]
};
const silentRequest = {
scopes: ["openid", "profile", "User.Read"]
};
const ua = window.navigator.userAgent;
const msie = ua.indexOf("MSIE ");
const msie11 = ua.indexOf("Trident/");
const msedge = ua.indexOf("Edge/");
const isIE = msie > 0 || msie11 > 0;
const isEdge = msedge > 0;
let signInType;
let accountId = "";
let credType = "";
// Create the main myMSALObj instance
const myMSALObj = new msal.PublicClientApplication(msalConfig);
// Register Callbacks for Redirect flow
myMSALObj.handleRedirectPromise().then(handleResponse).catch((error) => {
console.log(error);
});
function handleResponse(resp) {
alert("beginning handleResponse");
if (resp !== null) {
accountId = resp.account.homeAccountId;
credType = resp.account.credentialType;
myMSALObj.setActiveAccount(resp.account);
alert("response not null (already auth), accountId: " + accountId + ", credType: " + credType);
}
else {
const currentAccounts = myMSALObj.getAllAccounts();
if (!currentAccounts || currentAccounts.length < 1) {
alert("currentAccounts null/empty, going to signIn");
signIn("loginRedirect");
//return;
}
else if (currentAccounts.length > 1) {
// add choose account code here
alert("currentAccounts has multiple");
}
else if (currentAccounts.length === 1) {
const activeAccount = currentAccounts[0];
myMSALObj.setActiveAccount(activeAccount);
accountId = activeAccount.homeAccountId;
credType = activeAccount.credentialType;
alert("currentAccounts == 1; accountId: " + accountId + ", credType: " + credType);
}
}
}
async function signIn(method) {
signInType = isIE ? "loginRedirect" : method;
if (signInType === "loginPopup") {
return myMSALObj.loginPopup(loginRequest).then(handleResponse).catch(function (error) {
console.log(error);
});
}
else if (signInType === "loginRedirect") {
return myMSALObj.loginRedirect(loginRequest);
}
}
function signOut() {
const logoutRequest = {
account: myMSALObj.getAccountByHomeId(accountId)
};
myMSALObj.logoutRedirect(logoutRequest);
}
async function getTokenPopup(request, account) {
request.account = account;
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
console.log("acquiring token using popup");
return myMSALObj.acquireTokenPopup(request).catch(error => {
console.error(error);
});
}
else {
console.error(error);
}
});
}
// This function can be removed if you do not need to support IE
async function getTokenRedirect(request, account) {
request.account = account;
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
console.log("acquiring token using redirect");
myMSALObj.acquireTokenRedirect(request);
}
else {
console.error(error);
}
});
}
So what happens upon going to this page is I get the two alerts saying "beginning handleResponse" and then "currentAccounts null/empty, going to signIn."
Then I'm redirected to MS sign-in page which I do with my work MS account. This succeeds.
I'm then redirected to the site I have listed in Azure Reply URLs, another page on our site that isn't secure and has no Azure login code.
The problem is I have no idea where to check that the user is signed in. If I try and check immediately after the signIn("loginRedirect") call in the handleResponse() function on the first page, the code never gets hit apparently. If I try and check on the page I'm redirected to, by instantiating the MSAL object and calling getAllAccounts(), this returns null.
It seems maybe on the page I'm redirected to I could call the ssoSilent() function (seems like this can check if user is authenicated?), but this requires a username/AccountId parameter. Well I don't frickin know this if a user hasn't (possibly) been authenticated yet! I don't really understand that.
So I don't know. It's probably something stupid I'm doing but I'm a pretty basic JavaScript person and am pretty much a total noob with authenication stuff. Any help would be epic.

Linking Google account with existing account created using email in Parse.com

I have implemented google login in parse. Here is my code:
var querystring = require('querystring');
var _ = require('underscore');
var Buffer = require('buffer').Buffer;
var googleValidateEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo';
var TokenStorage = Parse.Object.extend("TokenStorage");
var restrictedAcl = new Parse.ACL();
restrictedAcl.setPublicReadAccess(false);
restrictedAcl.setPublicWriteAccess(false);
Parse.Cloud.define('accessGoogleUser', function(req, res) {
var data = req.params;
var token = data.code;
/**
* Validate that code and state have been passed in as query parameters.
* Render an error page if this is invalid.
*/
if (!(data && data.code)) {
res.error('Invalid auth response received.');
return;
}
Parse.Cloud.useMasterKey();
Parse.Promise.as().then(function() {
// Validate & Exchange the code parameter for an access token from Google
return getGoogleAccessToken(data.code);
}).then(function(httpResponse) {
var userData = httpResponse.data;
if (userData && userData.id) {
return upsertGoogleUser(token, userData, data.email);
} else {
return Parse.Promise.error("Unable to parse Google data");
}
}).then(function(user) {
/**
* Send back the session token in the response to be used with 'become/becomeInBackground' functions
*/
res.success(user.getSessionToken());
}, function(error) {
/**
* If the error is an object error (e.g. from a Parse function) convert it
* to a string for display to the user.
*/
if (error && error.code && error.error) {
error = error.code + ' ' + error.error;
}
res.error(JSON.stringify(error));
});
});
var getGoogleAccessToken = function(code) {
var body = querystring.stringify({
access_token: code
});
return Parse.Cloud.httpRequest({
url: googleValidateEndpoint + '?access_token=' + code
});
}
var upsertGoogleUser = function(accessToken, googleData, emailId) {
var query = new Parse.Query(TokenStorage);
query.equalTo('accountId', googleData.id);
//query.ascending('createdAt');
// Check if this googleId has previously logged in, using the master key
return query.first({ useMasterKey: true }).then(function(tokenData) {
// If not, create a new user.
if (!tokenData) {
return newGoogleUser(accessToken, googleData, emailId);
}
// If found, fetch the user.
var user = tokenData.get('user');
return user.fetch({ useMasterKey: true }).then(function(user) {
// Update the access_token if it is different.
if (accessToken !== tokenData.get('accessToken')) {
tokenData.set('accessToken', accessToken);
}
/**
* This save will not use an API request if the token was not changed.
* e.g. when a new user is created and upsert is called again.
*/
return tokenData.save(null, { useMasterKey: true });
}).then(function(obj) {
// Reset password
password = new Buffer(24);
_.times(24, function(i) {
password.set(i, _.random(0, 255));
});
password = password.toString('base64')
user.setPassword(password);
return user.save();
}).then(function(user) {
// ReLogin
// This line is what I am talking about
return Parse.User.logIn(user.get('username'), password);
}).then(function(obj) {
// Return the user object.
return Parse.Promise.as(obj);
});
});
}
var newGoogleUser = function(accessToken, googleData, email) {
var user = new Parse.User();
// Generate a random username and password.
var username = new Buffer(24);
var password = new Buffer(24);
_.times(24, function(i) {
username.set(i, _.random(0, 255));
password.set(i, _.random(0, 255));
});
var name = googleData.name;
// name = name.split(" ");
// var fullname = name;
// if(name.length > 1)
// var lastName = name[name.length-1];
user.set("username", username.toString('base64'));
user.set("password", password.toString('base64'));
user.set("email", email);
user.set("fullName", name);
// user.set("last_name", lastName);
user.set("accountType", 'google');
// Sign up the new User
return user.signUp().then(function(user) {
// create a new TokenStorage object to store the user+Google association.
var ts = new TokenStorage();
ts.set('user', user);
ts.set('accountId', googleData.id);
ts.set('accessToken', accessToken);
ts.setACL(restrictedAcl);
// Use the master key because TokenStorage objects should be protected.
return ts.save(null, { useMasterKey: true });
}).then(function(tokenStorage) {
return upsertGoogleUser(accessToken, googleData);
});
}
It works perfectly fine. Now the problem I am facing is that I want to link google account with an existing parse account created using email or username & password. The problem in doing so is that to login/signup using google I have to reset the password of the user to login so as to get the session token. See this line in the code -> [This line is what I am talking about]. So if I do so an existing user who had earlier used username/email & password to login won't be able to login again using email since I have reset his/her password. I have seen this and all the other links related to this but none of which solves this problem.
Can somebody here guide me in the right direction?
Log added as response to one of the comments:
{"accountType":"google","createdAt":"2016-01-07T17:30:57.429Z","email":"skdkaney#gmail.com","fullName":"ashdakhs basdkbney","updatedAt":"2016-01-07T17:30:57.429Z","username":"owt3h0ZZEZQ1K7if55W2oo3TBLfeWM6m","objectId":"lSlsdsZ9"}
Added upsert function as per comment request:
var upsertGoogleUser = function(accessToken, googleData, emailId) {
var query = new Parse.Query(TokenStorage);
query.equalTo('accountId', googleData.id);
//query.ascending('createdAt');
// Check if this googleId has previously logged in, using the master key
return query.first({ useMasterKey: true }).then(function(tokenData) {
// If not, create a new user.
if (!tokenData) {
return newGoogleUser(accessToken, googleData, emailId);
}
// If found, fetch the user.
var userw = tokenData.get('user');
var users_id = userw.id;
var query2 = new Parse.Query(Parse.User);
query2.equalTo('objectId',users_id);
// The new query added
return query2.first({ useMasterKey: true }).then(function(user) {
// Update the access_token if it is different.
// if (accessToken !== tokenData.get('accessToken')) {
// tokenData.set('accessToken', accessToken);
// }
console.log(user);
console.log("******");
/**
* This save will not use an API request if the token was not changed.
* e.g. when a new user is created and upsert is called again.
*/
// return tokenData.save(null, { useMasterKey: true });
}).then(function(obj) {
console.log(obj);
// console.log(user);
var result = user ;
// Return the user object.
return Parse.Promise.as(result); // this is the user object acquired above
});
After a discussion with OP, there are possible solutions to this matter but each of them have pros and cons.
Disabling Revocable Session
Since the introduction of Revocable Session, getSessionToken will always return undefined even with master key. To turn it off, go to App Settings >> Users >> Turn off Require revocable sessions.
Then, in upsertGoogleUser method, you just need to return the user object from tokenData.get('user'). It is enough to call user.getSessionToken() in your main cloud function. The final method should look like:
var upsertGoogleUser = function(accessToken, googleData, emailId) {
Parse.Cloud.useMasterKey();
var query = new Parse.Query(TokenStorage);
query.equalTo('accountId', googleData.id);
//query.ascending('createdAt');
// Check if this googleId has previously logged in, using the master key
return query.first().then(function(tokenData) {
// If not, create a new user.
if (!tokenData) {
return newGoogleUser(accessToken, googleData, emailId);
}
// If found, fetch the user.
var userw = tokenData.get('user');
var users_id = userw.id;
var query2 = new Parse.Query(Parse.User);
query2.equalTo('objectId',users_id);
return query2.first().then(function(user) {
console.log(user);
console.log(user.getSessionToken());
console.log("******");
return Parse.Promise.as(user);
});
});
};
User Password Input
In order not to change user's password, we can ask user to input his password once we successfully authenticated Google data. We then use the input password to log user in. This is not a good UX, since the purpose of Google Login is to increase usability by letting users not entering password.
Query on Parse.Session
This is a possible solution if you want to use "Revocable Session" feature. In the code above, instead of querying on Parse.User, we can look for any revocable session in Parse.Session class. Then we can call getSessionToken on returned object. This is not optimal solution in cases that we need to know which devices the user is logged in.
Reference:
Parse's Enhanced Session: http://blog.parse.com/announcements/announcing-new-enhanced-sessions/

Push notifications (Parse/Cloud Code) not showing username for comments, or any likes

I'm using Parse and Cloud Code for the first time to update a photo-sharing app that was built with Parse and Objective-C for iOS 8. I've followed Parse's instructions for setting up push notifications and was successfully able to receive a notification when a photo was commented on, but it is only delivering the "else" statement that says "Someone has commented on your photo" (instead of giving user's name). Aside from that, 'like' notifications aren't being pushed at all.
Here's my cloud code (JavaScript) that I modeled after Parse's AnyPic tutorial. I also have an installation file and a "main" file that point to this one and the installation. What am I missing here?
Parse.Cloud.beforeSave('Activity', function(request, response) {
var currentUser = request.user;
var objectUser = request.object.get('fromUser');
if(!currentUser || !objectUser) {
response.error('An Activity should have a valid fromUser.');
} else if (currentUser.id === objectUser.id) {
response.success();
} else {
response.error('Cannot set fromUser on Activity to a user other than the current user.');
}
});
Parse.Cloud.afterSave('Activity', function(request) {
// Only sends push notifications for new activities.
if (request.object.existed()) {
return;
}
var toUser = request.object.get("toUser");
if (!toUser) {
throw "Undefined toUser. Skipping push for Activity " + request.object.get('type') + " : " + request.object.id;
return;
}
var query = new Parse.Query(Parse.Installation);
query.equalTo('user', toUser);
Parse.Push.send({
where: query, // Set our Installation query.
data: alertPayload(request)
}).then(function() {
// Push was successful
console.log('Sent push.');
}, function(error) {
throw "Push Error " + error.code + " : " + error.message;
});
});
var alertMessage = function(request) {
var message = "";
if (request.object.get("type") === "comment") {
if (request.user.get('displayName')) {
message = request.user.get('displayName') + ': ' + request.object.get('content').trim();
} else {
message = "Someone left a comment on your photo.";
}
} else if (request.object.get("type") === "like") {
if (request.user.get('displayName')) {
message = request.user.get('displayName') + ' likes your photo.';
} else {
message = 'Someone likes your photo.';
}
}
// Trim our message to 140 characters.
if (message.length > 140) {
message = message.substring(0, 140);
}
return message;
}
var alertPayload = function(request) {
var payload = {};
if (request.object.get("type") === "comment") {
return {
alert: alertMessage(request), // Sets the alert message.
badge: 'Increment', // Increments the target device's badge count.
// The following keys help Cosplace to load the photo corresponding to this push notification.
p: 'a', // Payload Type: Activity
t: 'c', // Activity Type: Comment
fu: request.object.get('fromUser').id, // From User
pid: request.object.id // Photo Id
};
} else if (request.object.get("type") === "like") {
return {
alert: alertMessage(request), // Sets the alert message.
// The following keys help Cosplace to load the photo corresponding to this push notification.
p: 'a', // Payload Type: Activity
t: 'l', // Activity Type: Like
fu: request.object.get('fromUser').id, // From User
pid: request.object.id // Photo Id
};
}
}
Thanks!
EDIT: Here's the Objective-C code from my AppDelagate.m file (I took out the Parse IDs):
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[Parse setApplicationId: #""
clientKey:#""];
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
// Reset application icon badge number when app is launched.
if (application.applicationIconBadgeNumber != 0) {
application.applicationIconBadgeNumber = 0;
[[PFInstallation currentInstallation] saveInBackground];
}
// Registers the current device for push notifications.
UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert |
UIUserNotificationTypeBadge |
UIUserNotificationTypeSound);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil];
[application registerUserNotificationSettings:settings];
[application registerForRemoteNotifications];
if (application.applicationState != UIApplicationStateBackground) {
// Track an app open here if we launch with a push, unless
// "content_available" was used to trigger a background push (introduced
// in iOS 7). In that case, we skip tracking here to avoid double
// counting the app-open.
BOOL preBackgroundPush = ![application respondsToSelector:#selector(backgroundRefreshStatus)];
BOOL oldPushHandlerOnly = ![self respondsToSelector:#selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)];
BOOL noPushPayload = ![launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (preBackgroundPush || oldPushHandlerOnly || noPushPayload) {
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
}
}
// If the registration is successful, this callback method will be executed. This method informs Parse about this new device.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// Reset application icon badge number when app is launched.
if (application.applicationIconBadgeNumber != 0) {
application.applicationIconBadgeNumber = 0;
// Store the deviceToken in the current installation and save it to Parse.
PFInstallation *currentInstallation = [PFInstallation currentInstallation];
// [PFInstallation.currentInstallation setObject:PFUser.currentUser forKey:#"user"];
[currentInstallation setDeviceTokenFromData:deviceToken];
// Associate the device with a user
PFInstallation *installation = [PFInstallation currentInstallation];
installation[#"user"] = [PFUser currentUser];
[installation saveInBackground];
[currentInstallation saveInBackground];
// Create Parse Installation query
PFQuery *pushQuery = [PFInstallation query];
[pushQuery whereKey:#"deviceType" equalTo:#"ios"];
}
}
// So when logged out notifications stop being received.
- (void)logOut {
// Unsubscribe from push notifications by removing the user association from the current installation.
[[PFInstallation currentInstallation] removeObjectForKey:#"user"];
[[PFInstallation currentInstallation] saveInBackground];
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
[PFPush handlePush:userInfo];
[[NSNotificationCenter defaultCenter] postNotificationName:#"AppDelegateApplicationDidReceiveRemoteNotification" object:nil userInfo:userInfo];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Clear badge and update installation for auto-incrementing badges.
if (application.applicationIconBadgeNumber != 0) {
application.applicationIconBadgeNumber = 0;
[[PFInstallation currentInstallation] saveInBackground];
}
}

How to build Twitter OAuth authentication?

This is my JavaScript code for following operation: https://api.twitter.com/1.1/users/show.json?screen_name=barackobama
Does not work anymore since the Twitter API 1.1: {"errors":[{"message":"Bad Authentication data","code":215}]}
I know you need the OAuth access Token now, I even created one in Twitter, but I don't know how to add it in this Script.
Thanks for any Help!
function TwitterFollowers(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.followers_count;
}
function TwitterFollowings(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.friends_count;
}
function TwitterListed(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.listed_count;
}
function TwitterId(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.id;
}
function TwitterFullname(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.name;
}
function TwitterCreatedDate(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.created_at;
}
function TwitterVerified(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.verified;
}
function TwitterTimezone(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.time_zone;
}
function TwitterLocation(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.location;
}
function TwitterHomepage(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.url;
}
function TwitterDescription(aUsername)
{
user = _twitterFetchUserData(aUsername);
return user.description;
}
// internal function invisible to Google SpreadSheets
var _twitterFetchUserData = function(aUsername)
{
if (aUsername === undefined || aUsername === null)
throw "No parameter specified. Write any Twitter USERNAME as parameter."
if (aUsername == "")
throw "USERNAME cannot be empty string. Write any Twitter USERNAME as parameter.";
// See https://dev.twitter.com/docs/api/1/get/users/show for API documentation
var url = "https://api.twitter.com/1.1/users/show.json?screen_name=" + encodeURIComponent(aUsername);
try
{
var response = UrlFetchApp.fetch(url);
}
catch (e)
{
throw "Please check if Twitter Username \"" + aUsername + "\" exists. " + e;
}
if (response.getResponseCode() != 200)
throw "Unexpected response code from Twitter.";
var responseText = response.getContentText();
if (responseText == null || responseText == "")
throw "Empty response from Twitter.";
var user = Utilities.jsonParse(responseText);
if (user == null)
throw "Problem with response from Twitter. Invalid JSON.";
return user;
};
According to the new version Twitter API v1.1 all requests to Twitter must be signed by using OAuth 1.0A. You may want to visit here to see how it works. I would suggest to use Twitter libraries to authenticate and sign your requests like codebird-js(have a look at the examples below or at the website given). It's really easy to use. To authenticate is as simple as below:
var cb = new Codebird;
cb.setConsumerKey('YOURKEY', 'YOURSECRET');
cb.setToken('YOURTOKEN', 'YOURTOKENSECRET');
and then make calls e.g. tweet:
cb.__call(
'statuses_update',
{'status': 'Whohoo, I just tweeted!'},
function (reply) {
// ...
}
);
Hope it helps.

socket.io cookie parse handshake error

When socket.io looks up a session, I get a handshake error because the id of the cookie does not match that from the database. I am using Express3, mongodb, connect-mongodb, and socket.io v0.9.10
For example, the result of the console.log(data.sessionID), from the socket.io code included, will print out:
s:eFFkUnQXWdTO7GBRDc11No/a.U6voj5QnxKs1skq766nO7/qJvPEJA73KaQM67qNEs/k
but when i look at the sessions collection on my database, I get the following _id:
"_id" : "eFFkUnQXWdTO7GBRDc11No/a",
As you can see this corresponds to the data.sessionID after the s: and before the period. I tried using two different cookie parsers. The code for the parser below is from the cookie module which is included in express 3. I am not sure if every cookie ID follows this pattern so I dont know if I should just parse the result again myself, or if there is something I am doing wrong.
exports.parseCookie = function(str) {
var obj = {}
var pairs = str.split(/[;,] */);
var encode = encodeURIComponent;
var decode = decodeURIComponent;
pairs.forEach(function(pair) {
var eq_idx = pair.indexOf('=')
var key = pair.substr(0, eq_idx).trim()
var val = pair.substr(++eq_idx, pair.length).trim();
// quoted values
if ('"' == val[0]) {
val = val.slice(1, -1);
}
// only assign once
if (undefined == obj[key]) {
obj[key] = decode(val);
}
});
return obj;
};
The code below is giving me a handshake error, becuase the 'connect.sid' propert does not match the id property from the database.
io.set('authorization', function (data, accept) {
if (data.headers.cookie) {
data.cookie = utils.parseCookie(data.headers.cookie);
data.sessionID = data.cookie['connect.sid'];
// **************** //
console.log(data.sessionID);
// **************** //
sessionStore.get(data.sessionID, function (err, session) {
if (err || ! session) {
accept('Error', false);
} else {
data.session = session;
data.session.url = data.headers.referer;
accept(null, true);
}
});
} else {
return accept('No cookie transmitted.', false);
}
});
I ran into the same issue and while your solution works, the parseCookie function you are using is from an older version of connect. The new code in connect now signs cookies, which is the extra characters after the period.
After some messing around in the guts of express and connect, I came up with the following code. While it depends on some a private API function of connect (utils.parseSignedCookies) it at least uses the same code to parse the cookie as was used to generate the cookie and should thus be a bit less sensitive to future changes in how the cookies are created and parsed.
/* cookie: the cookie string from the request headers
* sid: the session 'key' used with express.session()
* secret: the session 'secret' used with express.session()
*/
function parseSessionCookie(cookie, sid, secret) {
var cookies = require('express/node_modules/cookie').parse(cookie)
, parsed = require('express/node_modules/connect/lib/utils').parseSignedCookies(cookies, secret)
;
return parsed[sid] || null;
}
socketIO.set('authorization', function(data, accept) {
var sid = parseSessionCookie(data.headers.cookie, 'express.sid', 'secret');
if (sid) {
sessionStore.get(sid, function(err, session) {
if (err || !session) {
accept('Error', false);
} else {
data.session = session;
accept(null, true);
}
});
} else {
return accept('No cookie transmitted.', false);
}
});
It seems the sessionStore keys are now the shorter uid(24)-only version and no longer the 'long' version stored in the cookie.
For the moment I fixed it by doing a simple split('.')[0] to retrieve the uid(24) part:
data.sessionID = cookie['express.sid'].split('.')[0];

Categories