Downloading an attachment from Teams across a Bot Nodejs - botframework

I'm facing the following error when I try to get the attachment in Microsoft Teams with Bot Builder v4:
{"message":"Authorization has been denied for this request."}
everything works fine with the version 3, as far as I know in Teams is necessary a token in order get the binary array of the file.
In the v3 I'm able to get the jwt token in this way:
connector.getAccessToken.bind(connector)
and then I use it in the header of the GET request =>
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/octet-stream'
}
In the v4:
context.adapter.getUserToken(step.context, CONNECTION_SETTING_NAME);
is there another way to get a valid token in the v4?

To get the token just call the prompt again. You can find the auth sample here for node. The sample happens to use a waterfall dialog, which may not be needed in your case
let prompt = await step.prompt(OAUTH_PROMPT);
If the token is valid and not expired you can get the token like below, if the token is not valid or the user does not have a token they will be prompted to log in. Otherwise the token will be in the prompt result.
var tokenResponse = prompt.result;
if (tokenResponse != null) {
await step.context.sendActivity(`Here is your token: ${ tokenResponse.token }`);
}
These comments from the sample should help explain
// Call the prompt again because we need the token. The reasons for this are:
// 1. If the user is already logged in we do not need to store the token locally in the bot and worry
// about refreshing it. We can always just call the prompt again to get the token.
// 2. We never know how long it will take a user to respond. By the time the
// user responds the token may have expired. The user would then be prompted to login again.
//
// There is no reason to store the token locally in the bot because we can always just call
// the OAuth prompt to get the token or get a new token if needed.

these are the steps
/**
* WaterfallDialogStep to process the user's picture.
* #param {WaterfallStepContext} step WaterfallStepContext
*/
async processPhotoStep(step) {
await this.writeLogInTheStorage('Start downloading picture....');
await this.handleIncomingAttachment(step);
return await step.endDialog();
};
/**
* responds to the user with information about the saved attachment or an error.
* #param {Object} turnContext
*/
async handleIncomingAttachment(step) {
// Prepare Promises to download each attachment and then execute each Promise.
const attachment = step.context.activity.attachments[0];
const tokenIsRequired = await this.checkRequiresToken(step.context);
const dc = await this.dialogs.createContext(step.context);
const token = await dc.beginDialog(LOGIN_PROMPT); //await step.context.adapter.getUserToken(step.context, CONNECTION_SETTING_NAME);
let file = undefined;
if (tokenIsRequired) {
file = await this.downloadAttachment(token.result.token, attachment.contentUrl);
}
else {
file = await requestX(attachment.contentUrl);
}
await OAuthHelpers.postPhoto(step.context, token.result, file);
}
async downloadAttachment(token, url) {
const p = new Promise((resolve, reject) => {
request({
url: url,
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/octet-stream'
}
}, async function (err, response, body) {
const result = body
if (err) {
console.log(err);
//await this.writeLogInTheStorage('err 1 : ' + err);
reject(err);
} else if (result.error) {
console.log(result.error);
//await this.writeLogInTheStorage('err 2 : ' + err);
reject(result.error.message);
} else {
// The value of the body will be an array.
console.log(result);
//await this.writeLogInTheStorage('success : ' + result);
resolve(result);
}
});
});
return p;
}

Related

Extra information gets lost when updating user context

I've been fighting with this issue for days now and I just can't solve it. My app is built on React and Django Rest Framework. I'm authenticating users with JWT - when the user logs into the app, the React Auth context gets updated with some info about the tokens and I include some extra information in the context (namely the user email and some profile information) so that I have it easily accessible.
How I am doing this is by overwriting TokenObtainPairSerializer from simplejwt:
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
#classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token["email"] = user.email
token["information"] = Profile.objects.get(user=user).information
return token
On the frontend in my AuthContext.js:
const loginUser = async (email, password, firstLogin = false) => {
const response = await fetch(`${baseUrl}users/token/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrfToken,
},
body: JSON.stringify({
email,
password,
}),
});
const data = await response.json();
if (response.status === 200) {
setAuthTokens(data);
setUser(jwt_decode(data.access));
localStorage.setItem("authTokens", JSON.stringify(data));
if (firstLogin) {
history.push("/profile");
} else {
history.push("/");
}
} else {
return response;
}
};
Up to this point it works perfectly fine and my ReactDevTools show me that the AuthContext has all the data:
Now to the issue - once the access token has expired, the next API call the user makes gets intercepted to update the token. I do this in my axiosInstance:
const useAxios = () => {
const { authTokens, setUser, setAuthTokens } = useContext(AuthContext);
const csrfToken = getCookie("csrftoken");
const axiosInstance = axios.create({
baseURL,
headers: {
Authorization: `Bearer ${authTokens?.access}`,
"X-CSRFToken": csrfToken,
"Content-Type": "application/json",
},
});
axiosInstance.interceptors.request.use(async (req) => {
const user = jwt_decode(authTokens.access);
const isExpired = dayjs.unix(user.exp).diff(dayjs()) < 1;
if (!isExpired) return req;
const response = await axios.post(`${baseURL}users/token/refresh/`, {
refresh: authTokens.refresh,
});
// need to add user info to context here
localStorage.setItem("authTokens", JSON.stringify(response.data));
setAuthTokens(response.data);
setUser(jwt_decode(response.data.access));
req.headers.Authorization = `Bearer ${response.data.access}`;
return req;
});
return axiosInstance;
};
export default useAxios;
But the extra information is not there. I tried to overwrite the TokenRefreshSerializer from jwt the same way as I did it with the TokenObtainPairSerializer but it just doesn't add the information
class MyTokenRefreshSerializer(TokenRefreshSerializer):
#classmethod
def get_token(cls, user):
token = super().get_token(user)
token["email"] = user.email
token["information"] = Profile.objects.get(user=user).information
print(token)
return token
It doesn't even print the token in my console but I have no clue what else I should try here.
Before anyone asks, yes I specified that the TokenRefreshView should use the custom serializer.
class MyTokenRefreshView(TokenRefreshView):
serializer_class = MyTokenRefreshSerializer
However, after a while of being logged into the application, the email and information key value pairs disappear from the context.
Any idea about how this can be solved will be much appreciated!

Fitbit URL callback giving a response of NULL

I'm having trouble getting a response from a callback uri and I would really appreciate any help you could give me.
I am trying to use the Fitbit API which requires you to use a callback url to get an Auth Code.
Workflow:
1. Go to Fitbit url to get user to allow the app access to their personal data.
2. User agrees to the conditions
3. User gets redirected to my API
4. The API returns the code from (Code is located in URL and I can access it)
5. I console.log the code out to verify it
6. API returns the code
7. I work with code then exchanging it for an access token.
The problem is that I don't return the code (Or anything )when I return to the app even though I can console.log it on the API. The response I get is NULL
Here is the URL:
url = "https://www.fitbit.com/oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https://REDIRECT_URL&scope=activity%20heartrate%20location%20nutrition%20profile%20settings%20sleep%20social%20weight&expires_in=604800";
I then open the URL in the InAPPBrowser successfully:
if (url !== "") {
const canOpen = await Linking.canOpenURL(url)
if (canOpen) {
try {
const isAvailable = await InAppBrowser.isAvailable()
if (isAvailable) {
const result =InAppBrowser.open(url, {
// iOS Properties
dismissButtonStyle: 'done',
preferredBarTintColor: 'gray',
preferredControlTintColor: 'white',
// Android Properties
showTitle: true,
toolbarColor: '#6200EE',
secondaryToolbarColor: 'black',
enableDefaultShare: true,
}).then((result) => {
console.log("Response:",JSON.stringify(result))
Linking.getInitialURL().then(url => {
console.log("Tests: ",url)
this._setTracker(url as string);
});
})
} else Linking.openURL(url)
} catch (error) {
console.log("Error: ",error)
}
}
}
From here the URL opens successfully.
Here is the API now which is done in Typescript on AWS serverless and Lambda
export const handler: APIGatewayProxyHandler = async (event, _context, callback) =>{
let provider = event.path
//prints code
let x = event.queryStringParameters
console.log("Code: ",x)
const response = {
statusCode: 200,
body: "Success"
};
return response;
}
Please let me know if further detail is required?
Thank you!
Right so it turns out what I was doing was correct apart from the response should have been 301 which is a redirect response.
const response= {
statusCode: 301,
headers: {
"location": `app://CALLBACK RESPONSE ADDRESS?type=${provider}`
},
body: "Boom"
}

Messaging: The notification permission was not granted and blocked instead

Firebase messaging error in https server.
An error occurred while retrieving token. FirebaseError: Messaging: The notification permission was not granted and blocked instead. (messaging/permission-blocked).
What should I do to get my token?
On localhost, it's working.
Here is my code:
firebase-messaging-sw.js
// Import and configure the Firebase SDK
// These scripts are made available when the app is served or deployed on Firebase Hosting
// If you do not serve/host your project using Firebase Hosting see https://firebase.google.com/docs/web/setup
importScripts("https://www.gstatic.com/firebasejs/7.8.1/firebase-app.js")
importScripts("https://www.gstatic.com/firebasejs/7.8.1/firebase-messaging.js")
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: apiKey,
authDomain: authDomain,
databaseURL: databaseURL,
projectId: projectId,
storageBucket: storageBucket,
messagingSenderId: messagingSenderId,
appId: appId,
measurementId: measurementId
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
/**
* Here is is the code snippet to initialize Firebase Messaging in the Service
* Worker when your app is not hosted on Firebase Hosting.
// [START initialize_firebase_in_sw]
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in the
// messagingSenderId.
firebase.initializeApp({
'messagingSenderId': 'YOUR-SENDER-ID'
});
// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();
// [END initialize_firebase_in_sw]
**/
// If you would like to customize notifications that are received in the
// background (Web app is closed or not in browser focus) then you should
// implement this optional method.
// [START background_handler]
// [END background_handler]
messaging.setBackgroundMessageHandler(function(payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
// Customize notification here
var notificationTitle = 'Background Message Title';
var notificationOptions = {
body: 'Background Message body.',
icon: '/firebase-logo.png'
};
return self.registration.showNotification(notificationTitle,
notificationOptions);
});
footer.blade.php
in //script// section:
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: apiKey,
authDomain: authDomain,
databaseURL: databaseURL,
projectId: projectId,
storageBucket: storageBucket,
messagingSenderId: messagingSenderId,
appId: appId,
measurementId: measurementId
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const tokenDivId = 'token_div';
const permissionDivId = 'permission_div';
const messaging = firebase.messaging();
messaging.usePublicVapidKey('BKotWNDl7JOuYb-UeusSlSl47onAFH9sWJ_M1WDivsjWq0AZWah5LjVfBAxbcS8T8Yo10HEw_xPX68kMnzTQC2k');
// Get Instance ID token. Initially this makes a network call, once retrieved
// subsequent calls to getToken will return from cache.
messaging.getToken().then((currentToken) => {
if (currentToken) {
sendTokenToServer(currentToken);
updateUIForPushEnabled(currentToken);
} else {
// Show permission request.
console.log('No Instance ID token available. Request permission to generate one.');
// Show permission UI.
updateUIForPushPermissionRequired();
setTokenSentToServer(false);
}
}).catch((err) => {
console.log('An error occurred while retrieving token. ', err);
showToken('Error retrieving Instance ID token. ', err);
setTokenSentToServer(false);
});
// Callback fired if Instance ID token is updated.
messaging.onTokenRefresh(() => {
messaging.getToken().then((refreshedToken) => {
console.log('Token refreshed.');
// Indicate that the new Instance ID token has not yet been sent to the
// app server.
setTokenSentToServer(false);
// Send Instance ID token to app server.
sendTokenToServer(refreshedToken);
// ...
}).catch((err) => {
console.log('Unable to retrieve refreshed token ', err);
showToken('Unable to retrieve refreshed token ', err);
});
});
function resetUI() {
clearMessages();
showToken('loading...');
// [START get_token]
// Get Instance ID token. Initially this makes a network call, once retrieved
// subsequent calls to getToken will return from cache.
messaging.getToken().then(function(currentToken) {
if (currentToken) {
sendTokenToServer(currentToken);
updateUIForPushEnabled(currentToken);
} else {
// Show permission request.
console.log('No Instance ID token available. Request permission to generate one.');
// Show permission UI.
updateUIForPushPermissionRequired();
setTokenSentToServer(false);
}
}).catch(function(err) {
console.log('An error occurred while retrieving token. ', err);
showToken('Error retrieving Instance ID token. ', err);
setTokenSentToServer(false);
});
// [END get_token]
}
function showToken(currentToken) {
// Show token in console and UI.
var tokenElement = document.querySelector('#token');
tokenElement.textContent = currentToken;
}
// Send the Instance ID token your application server, so that it can:
// - send messages back to this app
// - subscribe/unsubscribe the token from topics
function sendTokenToServer(currentToken) {
if (!isTokenSentToServer()) {
console.log('Sending token to server...');
// TODO(developer): Send the current token to your server.
setTokenSentToServer(true);
} else {
console.log('Token already sent to server so won\'t send it again ' +
'unless it changes');
}
}
function isTokenSentToServer() {
return window.localStorage.getItem('sentToServer') === '1';
}
function setTokenSentToServer(sent) {
window.localStorage.setItem('sentToServer', sent ? '1' : '0');
}
function showHideDiv(divId, show) {
const div = document.querySelector('#' + divId);
}
function requestPermission() {
console.log('Requesting permission...');
// [START request_permission]
messaging.requestPermission().then(function() {
console.log('Notification permission granted.');
// TODO(developer): Retrieve an Instance ID token for use with FCM.
// [START_EXCLUDE]
// In many cases once an app has been granted notification permission, it
// should update its UI reflecting this.
resetUI();
// [END_EXCLUDE]
}).catch(function(err) {
console.log('Unable to get permission to notify.', err);
});
// [END request_permission]
}
function deleteToken() {
// Delete Instance ID token.
// [START delete_token]
messaging.getToken().then(function(currentToken) {
messaging.deleteToken(currentToken).then(function() {
console.log('Token deleted.');
setTokenSentToServer(false);
// [START_EXCLUDE]
// Once token is deleted update UI.
resetUI();
// [END_EXCLUDE]
}).catch(function(err) {
console.log('Unable to delete token. ', err);
});
// [END delete_token]
}).catch(function(err) {
console.log('Error retrieving Instance ID token. ', err);
showToken('Error retrieving Instance ID token. ', err);
});
}
// Add a message to the messages element.
function appendMessage(payload) {
const messagesElement = document.querySelector('#messages');
const dataHeaderELement = document.createElement('h5');
const dataElement = document.createElement('pre');
dataElement.style = 'overflow-x:hidden;';
dataHeaderELement.textContent = 'Received message:';
dataElement.textContent = JSON.stringify(payload, null, 2);
messagesElement.appendChild(dataHeaderELement);
messagesElement.appendChild(dataElement);
}
// Clear the messages element of all children.
function clearMessages() {
const messagesElement = document.querySelector('#messages');
while (messagesElement.hasChildNodes()) {
messagesElement.removeChild(messagesElement.lastChild);
}
}
function updateUIForPushEnabled(currentToken) {
showHideDiv(tokenDivId, true);
showHideDiv(permissionDivId, false);
showToken(currentToken);
}
function updateUIForPushPermissionRequired() {
showHideDiv(tokenDivId, false);
showHideDiv(permissionDivId, true);
}
This indicates that you have blocked the push notifications permission on the deployed website. It's messaging.getToken() that is likely erroring out (see the docs for more).
If you're using Chrome, you should be able to click the lock to the left of the URL and go to "Site Settings" where you'll see a bell icon with the notification settings for the site:
This may be set to "Block" and you would need to change it to "Allow" instead.
Make sure you're not in an incognito tab - in that case the permissions dialog (Allow/Block) won't show up (in Chrome at least)

How to subscribe to the refresh token event

The problem is that I need to be able to subscribe to the token refresh event and I can't figure out how.
I know people advise on subscribing to connectionStatus$ and handling the ConnectionStatus.ExpiredToken case, but the execution never enters that case when refreshing happens, it only enters that case when I try to initialize the bot with an expired token.
The token refresh event is getting triggered every 15 minutes by the library itself but there is no observable that will allow me to subscribe to it to get the newly refreshed token. The workaround I found for now is that I set an interval of 15 min (by using setInterval()) for checking if the token used by the connection has changed.
Any idea?
The code is in pure, vanilla, javascript.
Library code I'm using: https://cdn.botframework.com/botframework-webchat/master/webchat.js
If you are wanting to capture the token, then I would recommend you save it during the API call you make to retrieve it. As you haven't provided any code, I can only guess at what your setup looks like. The below is a simplified example demonstrating getting a token (and saving it to sessionStorage) if no token exists. If a token does exist it's then refreshed every 25 mins (and saved to sessionStorage) as at 30 mins it is in danger of expiring with no activity.
let { token, conversationId } = sessionStorage;
if ( !token ) {
let res = await fetch( 'http://localhost:3500/directline/token', { method: 'POST' } );
const { token: directLineToken, conversationId, error } = await res.json();
sessionStorage[ 'token' ] = directLineToken;
sessionStorage[ 'conversationId' ] = conversationId;
token = directLineToken;
}
if (token) {
await setInterval(async () => {
let res = await fetch( 'http://localhost:3500/directline/refresh', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify( { token: token } )
} );
const { token: directLineToken, conversationId } = await res.json();
sessionStorage[ 'token' ] = directLineToken;
sessionStorage[ 'conversationId' ] = conversationId;
token = directLineToken;
}, 150000)
}
It is possible to subscribe to connectionStatus$, however that will only show if a connection was made but an error was encountered regarding that connection. If there is a token issue that restricts Web Chat from even connecting, then the observable will never be reached.
const store = window.WebChat.createStore( {}, ( { dispatch } ) => next => async action => {
if(action.payload && action.payload.directLine) {
const subscription = action.payload.directLine.connectionStatus$.subscribe( {
error: error => console.log( error ),
next: value => {
if ( value === 2 ) {
console.log('Connected')
} else if ( value === 4 ) {
console.log('Encountered a connection error')
}
}
} );
}
}
Hope of help!

Parse server twitter authentication: Twitter auth integrated but unable to create session to use on client side

Parse Cloud code:
Parse.Cloud.define("twitter", function(req, res) {
/*
|--------------------------------------------------------------------------
| Login with Twitter
| Note: Make sure "Request email addresses from users" is enabled
| under Permissions tab in your Twitter app. (https://apps.twitter.com)
|--------------------------------------------------------------------------
*/
var requestTokenUrl = 'htt****/oauth/request_token';
var accessTokenUrl = 'http***itter.com/oauth/access_token';
var profileUrl = 'https://api.twitter.com/1.1/account/verify_credentials.json';
// Part 1 of 2: Initial request from Satellizer.
if (!req.params.oauth_token || !req.params.oauth_verifier) {
var requestTokenOauth = {
consumer_key: 'EVJCRJfgcKSyNUQgOhr02aPC2',
consumer_secret: 'UsunEtBnEaQRMiq5yi4ijnjijnjijnijnjEjkjYzHNaaaSbQCe',
oauth_callback: req.params.redirectUri
};
// Step 1. Obtain request token for the authorization popup.
request.post({
url: requestTokenUrl,
oauth: requestTokenOauth
}, function(err, response, body) {
var oauthToken = qs.parse(body);
// console.log(body);
// Step 2. Send OAuth token back to open the authorization screen.
console.log(oauthToken);
res.success(oauthToken);
});
} else {
// Part 2 of 2: Second request after Authorize app is clicked.
var accessTokenOauth = {
consumer_key: 'EVJCRJfgcKSyNUQgOhr02aPC2',
consumer_secret: 'UsunEtBnEaQRMiq5yi4ijnjijnjijnijnjEjkjYzHNaaaSbQCe',
token: req.params.oauth_token,
verifier: req.params.oauth_verifier
};
// Step 3. Exchange oauth token and oauth verifier for access token.
request.post({
url: accessTokenUrl,
oauth: accessTokenOauth
}, function(err, response, accessToken) {
accessToken = qs.parse(accessToken);
var profileOauth = {
consumer_key: 'EVJCRJfgcKSyNUQgOhr02aPC2',
consumer_secret: 'UsunEtBnEaQRMiq5yi4ijnjijnjijnijnjEjkjYzHNaaaSbQCe',
token: accessToken.oauth_token,
token_secret: accessToken.oauth_token_secret,
};
console.log(profileOauth);
// Step 4. Retrieve user's profile information and email address.
request.get({
url: profileUrl,
qs: {
include_email: true
},
oauth: profileOauth,
json: true
}, function(err, response, profile, USER) {
console.log(profile);
//console.log(response.email);
Parse.Cloud.useMasterKey();
var UserPrivateInfo = Parse.Object.extend("UserPrivateInfo");
var query = new Parse.Query(UserPrivateInfo);
query.equalTo("email", profile.email);
query.first({
success: function(privateInfo) {
if (privateInfo) {
res.success(privateInfo.get('user'));
} else {
response.success();
}
},
error: function(error) {
response.error("Error : " + error.code + " : " + error.message);
}
});
});
});
}
});
For client side using Sendgrid twitter authentication:
loginCtrl.twitterLogin = function() {
$auth.authenticate("twitter").then(function(response) {
console.log(response.data.result);
var user = response.data.result;
if (!user.existed()) {
var promise = authFactory.saveUserStreamDetails(user, response.email);
promise.then(function(response) {
signInSuccess(response);
}, function(error) {
console.log("error while saving user details.");
});
} else {
signInSuccess(user);
}
}).catch(function(error) {
console.log(error);
});;
};
Issue:
Step 1: Called cloud function Parse.Cloud.define("twitter", function(req, res) using loginCtrl.twitterLogin
Step 2: Twitter popup opens and user logs in to twitter
Step 3: Got verification keys and again cloud function Parse.Cloud.define("twitter", function(req, res) is called and user is verified
Step 4: Got the user email using the twitter API.
Step 5: I can get the existing Parse User Object using the email or can signUp using that email.
Step 6: Returns the parse user object to client but there is no session attached to it so **How can I create user session?
Without parse session we can not log in to parse application. Every clound code api/ function call will fail as no session is attached to them. So how can I create and manage a session using twitter authentication.

Resources