How to subscribe to the refresh token event - botframework

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!

Related

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"
}

"[Network] undefined" when trying to use subscriber - URQL

I am trying to set up a subscriber to log some output on the creation of a new Message.
Currently using Urql, with ApolloServerExpress on the backend.
I am receiving an error from the useSubscription method which I am logging to the console :
message: "[Network] undefined"
name: "CombinedError"
I know for sure my backend is working as I can subscribe using the Graphiql playground just fine.
As far as front end goes, I have followed almost exactly as the example in the Urql docs.
WS Client:
const wsClient = createWSClient({
url: "ws://localhost:4000/graphql",
});
Subscriber Exchange:
subscriptionExchange({
forwardSubscription(operation) {
return {
subscribe: (sink) => {
const dispose = wsClient.subscribe(operation, sink);
return {
unsubscribe: dispose,
};
},
};
},
}),
MessageList Component:
const newMessages = `
subscription Messages {
newMessage {
content
status
sender {
id
email
}
recipient {
id
email
}
}
}
`;
...
const handleSub = (messages: any, newMessage: any) => {
console.log("Messages: ", messages);
console.log("newMessages: ", newMessage);
};
const [res] = useSubscription({ query: newMessages }, handleSub);
console.log("Res: ", res);
I was getting the same error when using subscriptions with urql. In my case, I was able to do console.log(error.networkError); which gave a much more helpful error message than [Network] undefined.
You can read more about errors in urql here.
The error I got from error.networkError was:
Event {
"code": 4400,
"isTrusted": false,
"reason": "{\"server_error_msg\":\"4400: Connection initialization failed: Missing 'Authorization' or 'Cookie' header in JWT authenticati",
}
I was able to fix it by adding authentication to my subscription exchange setup. Here's the code I'm using now:
const wsClient = createWSClient({
url: "wss://your-api-url/graphql",
connectionParams: async () => {
// Change this line to however you get your auth token
const token = await getTokenFromStorage();
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
},
});
Ended up chalking graphql-ws and switched over to subscriptions-transport-ws.
Fixed my issues.

Resending a graphql mutation after re-authenticating using Apollo's useMutation

I have an issue where we're using apollo client and specifically the useMutation react hook to perform mutation calls to our GraphQL Server.
At certain times, the server may return a 401 unauthorized response - at which point, we can make a call to special endpoint which re-authenticates the client and refreshes the cookie/token whatever.
I want to be able to re-run the same mutation again once the client is re-authenticated. So basically I would like to know if it is possible to do the following:
useMutation --> Receive 401 Unauthorized --> call to refresh token --> rerun same initial mutation
This is how our useMutation looks like:
const [mutationFunction, { data, ...rest }] = useMutation(query, {
onError(_err: any) {
const networkError = error?.networkError as any;
if (networkError?.statusCode === 401 && !refreshFailed) {
// eslint-disable-next-line prefer-destructuring
loading = true;
error = undefined;
fetch('/authentication/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(token => {
localStorage.setItem(jwtLocalStorageKey, token);
// re fetch here
})
.catch(() => {
refreshFailed = true;
});
} else {
showAlert(_err.message, 'error');
}
}
});
and this is how we call it currently:
const {
mutationFunction: updateTournamentUserMutation,
loading: updateTournamentUserLoading,
error: updateTournamentUserError,
data: updateTournamentUserData
} = useMutationHook(gqlUpdateTournamentUser);
updateTournamentUserMutation({ variables: { input } });
Because we're using hooks and the way we're using it above, I'm not entirely sure how we can save or reuse the same data that is initially sent in the first mutation (that is the mutation parameters)
Is it possible to do so using the current way we're doing it?

Downloading an attachment from Teams across a Bot Nodejs

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;
}

Ember Simple Auth on Firefox: authentication throws Error

I am extending Ember Simple Auth's base authentication class to allow authentication with Google. So far, it works on Safari 8 and Chrome 41 (both on Yosemite) with no errors. However, on Firefox 35, it throws an Error that does not occur on the other browsers. Here is my Google authenticator class:
App.GoogleAuthenticator = SimpleAuth.Authenticators.Base.extend({
// constants for Google API
GAPI_CLIENT_ID: 'the client id',
GAPI_SCOPE: ['email'],
GAPI_TOKEN_VERIFICATION_ENDPOINT: 'https://www.googleapis.com/oauth2/v2/tokeninfo',
// method for scheduleing a single token refresh
// time in milliseconds
scheduleSingleTokenRefresh: function(time) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.run.later(self, function() {
gapi.auth.authorize({
client_id: self.GAPI_CLIENT_ID,
scope: self.GAPI_SCOPE,
immediate: true
}, function(data) {
if (data && !data.error) {
resolve(data);
} else {
reject((data || {}).error);
}
});
}, time);
});
},
// WIP: recursive method that reschedules another token refresh after the previous scheduled one was fulfilled
// usage: scheduleTokenRefreshes(time until token should refresh for the first time, time between subsequent refreshes)
// usage: scheduleTokenRefreshes(time between refreshes)
scheduleTokenRefreshes: function(time1, time2) {
var self = this;
// if there is a time2, schedule a single refresh, wait for it to be fulfilled, then call myself to schedule again
if (!Ember.isEmpty(time2)) {
self.scheduleSingleTokenRefresh(time1)
.then(function() {
self.scheduleTokenRefreshes(time2);
});
// if there isn't a time2, simply schedule a single refresh, then call myself to schedule again
} else {
self.scheduleSingleTokenRefresh(time1)
.then(function() {
self.scheduleTokenRefreshes(time1);
});
}
},
// method that restores the session on reload
restore: function(data) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
console.log(data);
if (Ember.isEmpty(data.access_token)) {
reject();
return;
}
// schedule a refresh 15 minutes before it expires or immediately if it expires in < 15
var timeNow = Math.floor(Date.now() / 1000);
var expiresAt = +data.expires_at;
var timeDifference = expiresAt - timeNow;
var schedulingDelay = Math.floor(timeDifference - 15 * 60);
schedulingDelay = schedulingDelay < 0 ? 0 : schedulingDelay;
self.scheduleTokenRefreshes(schedulingDelay * 1000, 45 * 60);
resolve(data);
});
},
// method that authenticates
authenticate: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
gapi.auth.authorize({
client_id: self.GAPI_CLIENT_ID,
scope: self.GAPI_SCOPE
}, function(data) {
if (data && !data.error) {
// schedule a refresh in 45 minutes
var schedulingDelay = 45 * 60;
self.scheduleTokenRefreshes(schedulingDelay * 1000);
resolve(data);
} else {
reject((data || {}).error);
}
});
});
},
// method that logs the user out and revokes the token
invalidate: function(data) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
// send a GET request to revoke the token
Ember.$.ajax({
type: 'GET',
url: 'https://accounts.google.com/o/oauth2/revoke?token=' + self.get('session.access_token'),
contentType: 'application/json',
dataType: 'jsonp'
})
.done(function(successData) {
resolve(successData);
})
.fail(function(error) {
reject(error);
});
});
}
});
When the popup window closes after a successful login on Google's end, this error appears on Firefox's console:
Error: Assertion Failed: Error: Permission denied to access property 'toJSON' ember.js:13749
"__exports__.default<.persist#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1524:1
__exports__.default<.updateStore#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1195:11
__exports__.default<.setup#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1149:9
__exports__.default<.authenticate/</<#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1066:13
tryCatch#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:47982:16
invokeCallback#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:47994:17
publish#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:47965:11
#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:29462:9
Queue.prototype.invoke#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:848:11
Queue.prototype.flush#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:913:13
DeferredActionQueues.prototype.flush#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:718:13
Backburner.prototype.end#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:143:11
createAutorun/backburner._autorun<#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:546:9
" ember.js:29488
Here is the version information:
DEBUG: Ember : 1.9.1
DEBUG: Ember Data : 1.0.0-beta.14.1
DEBUG: Handlebars : 2.0.0
DEBUG: jQuery : 2.1.3
DEBUG: Ember Simple Auth : 0.7.2
The most confounding thing is that this only appears on Firefox. Is it a bug in Ember Simple Auth or Ember? How do I fix it?
I do not know about only Firefox throwing an error (I've had a similar error with Chrome 40), but there is a bug in ember-simple-auth 0.7.2 with Ember 1.9 that prohibits sending an actual error response in the authenticate method in the authenticator.
If you return reject() in the rejection function of authenticate it will not throw an additional error. This will however not propagate the errorstatus or message, so I consider this a bug.
A work-around was proposed on github about this issue by setting Ember.onerror=Ember.K temporarily so additional errors will not be propagated, although it will propagate the original authenticate rejection with the error-status.
The issue in the github repo only mentions problems with testing this, but I've had this problem in normal code.
see: https://github.com/simplabs/ember-simple-auth/issues/407
Turns out the error was on the resolve part of the authenticate method. Here is what fixed it:
App.GoogleAuthenticator = SimpleAuth.Authenticators.Base.extend({
authenticate: function() {
return new Ember.RSVP.Promise(function(resolve, reject) {
gapi.auth.authorize({
client_id: 'the client id',
scope: ['the scopes'],
}, function(data) {
if (data && !data.error) {
resolve({
access_token: data.access_token // !! passing the entire 'data' object caused the error somehow
});
} else {
reject((data || {}).error);
}
});
});
},
// ...
});
I'm still not quite sure why this caused the error. Perhaps the Google API's response (in its entirety) is somehow incompatible with Ember Simple Auth.

Resources