Can't get conversationUpdate activity with the Enhanced Direct Line Authentication Features - botframework

I'm trying to use the Enhanced Direct Line Authentication Features so I can get rid of the Magic Number.
I just enabled this option and added the trusted origin (https://mychatbot.azurewebsites.net/ <- Not the real one, but is stored on Azure) to the DirectLine.
Then on the code of the website I request the token:
const options = {
method: 'POST',
uri: 'https://directline.botframework.com/v3/directline/tokens/generate',
headers: {
"Authorization": "Bearer MyDirectLineSecret"
},
json: {
User: {
id: "dl_" + uuid.v4(),
name: "UserTest"
},
trustedOrigins: ["https://mychatbot.azurewebsites.net/"]
}
Then I make the request for the token:
const response = await rp(options);
const token = response.token;
Like that I have the token and when I go to my bot website (https://mychatbot.azurewebsites.net/) I don't send the updateActivity request and can't send the user the welcome message.
I don't know if I'm doing something wrong about the DirectLine configuration.
Is there anything I should change? I'm using an app service for the bot framework and inserting directly the webchat uri in the trusted origins. I don't know if I am wrong in the request of the token.

You aren't doing anything wrong. This is a known issue in the DirectLine Connector Service, and the development team is currently working to resolve the issue. Essentially, the second conversation update is not being sent because the user id in the token is causing an error. For more details, checkout this issue on Github. I'll be sure to let you know when it is resolved as well.
In the meantime, I would recommend taking a look at the Web Chat Backchannel Welcome Event sample.

Related

Retrieving a trusted client token within a Keycloak addon (SPI)

As part of our effort to be GDPR compliant, I’m trying to extend our Keycloak server with some custom functionality, to be triggered when a user is removed:
sending a confirmation message to a Slack channel
sending a HTTP request to another service we built, to delete any assets that user might have created there
The first one was easy. It's implemented as a SPI event listener, following this example:
#Override
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
keycloakSessionFactory.register(
(event) -> {
// the user is available via event.getUser()
if (event instanceof UserModel.UserRemovedEvent){
userRemovedMessageHandler.handleUserRemoveEvent(session, (UserRemovedEvent) event);
LOG.debug("User removed event happened for user: " + ((UserRemovedEvent) event).getUser().getId());
}
});
}
But I'm stuck on the second task (sending the HTTP request). The other service is set up to require a trusted client token, using a designated Keycloak Client and scope. The equivalent POST request to auth/realms/{realm}/protocol/openid-connect/token is done with these parameters:
grant_type: password
username: {user that is about to be deleted}
password: {user password}
client_id: {the specific client}
client_secret: {that client's secret}
scope: usersets
I can’t find out how to do that from within the SPI code. I can retrieve a regular Access Token (as described in this other SO post):
private String getAccessToken(KeycloakSession session, UserModel deleteUser) {
KeycloakContext keycloakContext = session.getContext();
AccessToken token = new AccessToken();
token.subject(deleteUser.getId());
token.issuer(Urls.realmIssuer(keycloakContext.getUri().getBaseUri(), keycloakContext.getRealm().getName()));
token.issuedNow();
token.expiration((int) (token.getIat() + 60L)); //Lifetime of 60 seconds
KeyWrapper key = session.keys().getActiveKey(keycloakContext.getRealm(), KeyUse.SIG, "RS256");
return new JWSBuilder().kid(key.getKid()).type("JWT").jsonContent(token).sign(
new AsymmetricSignatureSignerContext(key));
}
but that's not the token I need. In order to retrieve the required trusted client token I’d need the internal Keycloak / SPI-equivalent of the above POST request, but I can’t figure out how to do that or where to look.
Besides, I'm not sure if it is even possible to retrieve a trusted client token for a user that is in the process of being deleted. In other words, I don’t know if the UserRemovedEvent is fired before, during or after the user is actually deleted, or if Keycloak waits until any custom EventHandlers have finished running.
(In order to figure that out, I'll try if it is possible to retrieve that token using a regular http POST request from within the add-on code, but I have no idea if it's possible to connect to Keycloak like that from inside. I'll update the question if that works.)
I'd greatly appreciate any suggestion!
(I also asked this in the Keycloak discourse group here but it looks as if it will remain unanswered there)

Azure Bot - Directline token generation from html page hiding secret

I was trying to do some work around with azure bot service using Direct Line Channel from html page.
Script within html page is as follows:
index.html
var directLine = new window.WebChat.createDirectLine({ secret: 'SECRET' });
directLine.postActivity({
from: { id: 'myUserId', name: 'myUserName' }, // required (from.name is optional)
type: 'message',
text: 'hi'
}).subscribe(
id => console.log("Posted activity, assigned ID ", id),
error => console.log("Error posting activity", error)
);
directLine.activity$
.filter(activity => activity.type === 'message')
.subscribe(
message => console.log("received message ", message)
);
I found API "https://directline.botframework.com/v3/directline/tokens/generate" where secret can be exchanged with token but SECRET has to be added in Authorization header.
Is there a way to hide SECRET in html page without using MVC architecture? Or any other method to interact without exposing SECRET key.
No, there is no way to hide the secret. If it's in the web page, then it is accessible to anyone who inspects the source.
You don't necessarily have to opt for an MVC setup, however. All you need to do is create a service with APIs you can then access.
If you look over the latter half of this solution I previously provided, I demonstrate a simple setup that I run locally for development purposes. From the page hosting the Web Chat instance, I make a call to my custom /directline/token endpoint. The service, appended to my bot's index.js file gets a token and returns it back for use in Web Chat.
In production, I put the "token server" in its own file, and deploy it with the web app. It runs in the background on the server remaining inaccessible (as a file) but accessible via the APIs. Just lock down the API resources and you should be good to go.

Botframework WebChat: unable to start new and clean conversation on other chat/browser

I'm using webchat to make make a simple web interface for my bot. My bot has authentication using BotAuth to connect to an Azure Active Directory.
When I log in it works fine but when I start a new converstation on another device it contiues my conversation when it should be a new clean conversation.
I used this https://github.com/Microsoft/BotFramework-WebChat but I doesn't work
$.ajax({
type: "POST",
headers: {
"Authorization": "Bearer " + "yupOGxl-odA.cwA.USk.zul_EXUwk54fWqKT_N8hmsWyXSWo5DHMYj0r7DQjaZI"
},
url: "https://directline.botframework.com/v3/directline/tokens/generate",
}).done(function (response) {
console.log(response)
BotChat.App({
directLine: {
token: response.token
},
user: { id: 'userid' },
bot: { id: 'botid' },
resize: 'detect'
}, document.getElementById("bot"));
});
when I start a new converstation on another device it contiues my conversation when it should be a new clean conversation.
It seems that you’d like to generate and pass a Direct Line Token to initiate BotChat. In this section: “Secrets and tokens”, you can find "A Direct Line token is a key that can be used to access a single conversation".
If you generate a new token via Ajax request and use it to initiate BotChat every time when new user logged in, a new conversation should be started with that token, you can use browser network tool to check the conversationId in response of starting a conversation.
On the other hand, if you use same Direct Line Token on another device/browser client(or tab), it will access the same conversation.
I have 2 users in my bot and somehow when I use it I'm logged in as the other person
As JasonSowers mentioned in his comment, you are specifying same userid when you initiate BotChat, you had better to get current logged-in user’s id and dynamically pass it to BotChat user property.
Note: In your code, you make ajax request for generating token based on your Direct Line Secret on JavaScript client side, which still expose the Direct Line Secret. Others can easily get your Direct Line Secret by checking your JavaScript code then put your bot on their website. If possible, you can create a backend service and put the code logic for generating token on server-side, which would help hide your secret from client side.

Gmail API suddenly stopped working with [Error: unauthorized_client]

Where I work we use Google Apps for Work. For the last 9 months we've been using the Gmail API (~2,000 requests per day) to pull in new emails for our support email accounts.
This is how we originally set it up:
Go to https://console.developers.google.com/project/
Click on the project (or create a new one)
Click on API's & Auth
Click on Credentials
Click on Create new Client ID
Click on Service account
Download a JWT (json) for the account.
Follow the node.js quickstart guide with an installed/native type token for the same account, and authorize it through the console. The JWT tokens did not work unless we did this step, once for each account.
We did this for each of our individual support email accounts to avoid having to turn on domain wide delegation for any of them in the admin console. We were then able to authenticate with the tokens using the officially supported npm library googleapis, similar to this:
var google = require('googleapis');
var jwtClient = new google.auth.JWT(
token.client_email,
null,
token.private_key,
['https://www.googleapis.com/auth/gmail.readonly'],
'supportemail#mycompany.com'
);
jwtClient.authorize(function(err, tokens) {
if (err) {
return cb(err);
}
var gmail = google.gmail('v1');
var requestOptions = {
auth: jwtClient,
userId: 'me',
id: messageId,
format: 'raw'
};
gmail.users.messages.get(requestOptions, function(err, response) {
if (err) {
return cb(err);
}
// do stuff with the response
});
});
Like I said, we used this for a long time and never had any issues. Yesterday around 10am MST every one of the accounts stopped being able to authenticate at the same time, with jwtClient.authorize() suddenly returning the error [Error: unauthorized_client].
I tried doing the same thing with a new token on a new service account (the web interface to get the token has changed quite a bit in the last 9 months), and it returns the same error.
The version of googleapis that we were using was 0.9.7, but we can't get JWT authentication to work on the newest version either.
We opened a ticket with the Google APIs support team, but the support person we spoke with had never read the Gmail API specs before and was ultimately unable to help us, so he redirected us here in order to get in touch with the API engineering support team.
We have noticed that authentication works if we enable the scope for domain wide delegation in the admin console, but we would prefer not to do that. We don't need to impersonate the accounts and would prefer to use an individual JWT for each account.
It turns out that the auth flow we were using was never supported, and probably was broken due to a bugfix on Google's part.
In the question comments #Brandon Jewett-Hall and #Steve Bazyl recommended that we use the installed app auth flow instead, as it allows for indefinite refreshing of access tokens and is supported.
More information about the different auth flows can be found in the Google API docs.

How do I authorise an app (web or installed) without user intervention?

Let's say that I have a web app ("mydriveapp") that needs to access Drive files in a background service. It will either own the files it is accessing, or be run in a Google Account with which the owner has shared the documents.
I understand that my app needs a refresh token, but I don't want to write the code to obtain that since I'll only ever do it once.
NB. This is NOT using a Service Account. The app will be run under a conventional Google account. Service Account is a valid approach in some situations. However the technique of using Oauth Playground to simulate the app can save a bunch of redundant effort, and applies to any APIs for which sharing to a Service Account is unsupported.
NB June 2022. It seems that Google have updated their verification requirements which adds additional steps (or negates the approach - depending on your point of view).
See recent comments for more detail
This can be done with the Oauth2 Playground at https://developers.google.com/oauthplayground
Steps:-
Create the Google Account (eg. my.drive.app#gmail.com) - Or skip this step if you are using an existing account.
Use the API console to register the mydriveapp (https://console.developers.google.com/apis/credentials/oauthclient?project=mydriveapp or just https://console.developers.google.com/apis/)
Create a new set of credentials. Credentials/Create Credentials/OAuth Client Id then select Web application
Include https://developers.google.com/oauthplayground as a valid redirect URI
Note the client ID (web app) and Client Secret
Login as my.drive.app#gmail.com
Go to Oauth2 playground
In Settings (gear icon), set
OAuth flow: Server-side
Access type: Offline
Use your own OAuth credentials: TICK
Client Id and Client Secret: from step 5
Click Step 1 and choose Drive API v3 https://www.googleapis.com/auth/drive (having said that, this technique also works for any of the Google APIs listed)
Click Authorize APIs. You will be prompted to choose your Google account and confirm access
Click Step 2 and "Exchange authorization code for tokens"
Copy the returned Refresh token and paste it into your app, source code or in to some form of storage from where your app can retrieve it.
Your app can now run unattended, and use the Refresh Token as described https://developers.google.com/accounts/docs/OAuth2WebServer#offline to obtain an Access Token.
NB. Be aware that the refresh token can be expired by Google which will mean that you need to repeat steps 5 onwards to get a new refresh token. The symptom of this will be a Invalid Grant returned when you try to use the refresh token.
NB2. This technique works well if you want a web app which access your own (and only your own) Drive account, without bothering to write the authorization code which would only ever be run once. Just skip step 1, and replace "my.drive.app" with your own email address in step 6. make sure you are aware of the security implications if the Refresh Token gets stolen.
See Woody's comment below where he links to this Google video https://www.youtube.com/watch?v=hfWe1gPCnzc
.
.
.
Here is a quick JavaScript routine that shows how to use the Refresh Token from the OAuth Playground to list some Drive files. You can simply copy-paste it into Chrome dev console, or run it with node. Of course provide your own credentials (the ones below are all fake).
function get_access_token_using_saved_refresh_token() {
// from the oauth playground
const refresh_token = "1/0PvMAoF9GaJFqbNsLZQg-f9NXEljQclmRP4Gwfdo_0";
// from the API console
const client_id = "559798723558-amtjh114mvtpiqis80lkl3kdo4gfm5k.apps.googleusercontent.com";
// from the API console
const client_secret = "WnGC6KJ91H40mg6H9r1eF9L";
// from https://developers.google.com/identity/protocols/OAuth2WebServer#offline
const refresh_url = "https://www.googleapis.com/oauth2/v4/token";
const post_body = `grant_type=refresh_token&client_id=${encodeURIComponent(client_id)}&client_secret=${encodeURIComponent(client_secret)}&refresh_token=${encodeURIComponent(refresh_token)}`;
let refresh_request = {
body: post_body,
method: "POST",
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
})
}
// post to the refresh endpoint, parse the json response and use the access token to call files.list
fetch(refresh_url, refresh_request).then( response => {
return(response.json());
}).then( response_json => {
console.log(response_json);
files_list(response_json.access_token);
});
}
// a quick and dirty function to list some Drive files using the newly acquired access token
function files_list (access_token) {
const drive_url = "https://www.googleapis.com/drive/v3/files";
let drive_request = {
method: "GET",
headers: new Headers({
Authorization: "Bearer "+access_token
})
}
fetch(drive_url, drive_request).then( response => {
return(response.json());
}).then( list => {
console.log("Found a file called "+list.files[0].name);
});
}
get_access_token_using_saved_refresh_token();
Warning May 2022 - this answer may not be valid any longer - see David Stein's comment
Let me add an alternative route to pinoyyid's excellent answer (which didn't work for me - popping redirect errors).
Instead of using the OAuthPlayground you can also use the HTTP REST API directly. So the difference to pinoyyid's answer is that we'll do things locally. Follow steps 1-3 from pinoyyid's answer. I'll quote them:
Create the Google Account (eg. my.drive.app#gmail.com) - Or skip this step if you are using an existing account.
Use the API console to register the mydriveapp (https://console.developers.google.com/apis/credentials/oauthclient?project=mydriveapp or just https://console.developers.google.com/apis/)
Create a new set of credentials (NB OAuth Client ID not Service Account Key and then choose "Web Application" from the selection)
Now, instead of the playground, add the following to your credentials:
Authorized JavaScript Sources: http://localhost (I don't know if this is required but just do it.)
Authorized Redirect URIs: http://localhost:8080
Screenshot (in German):
Make sure to actually save your changes via the blue button below!
Now you'll probably want to use a GUI to build your HTTP requests. I used Insomnia but you can go with Postman or plain cURL. I recommend Insomnia for it allows you to go through the consent screens easily.
Build a new GET request with the following parameters:
URL: https://accounts.google.com/o/oauth2/v2/auth
Query Param: redirect_uri=http://localhost:8080
Query Param: prompt=consent
Query Param: response_type=code
Query Param: client_id=<your client id from OAuth credentials>
Query Param: scope=<your chosen scopes, e.g. https://www.googleapis.com/auth/drive.file>
Query Param: access_type=offline
If your tool of choice doesn't handle URL encoding automagically make sure to get it right yourself.
Before you fire your request set up a webserver to listen on http://localhost:8080. If you have node and npm installed run npm i express, then create an index.js:
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('ok');
console.log(req)
});
app.listen(8080, function () {
console.log('Listening on port 8080!');
});
And run the server via node index.js. I recommend to either not log the whole req object or to run node index.js | less for the full output will be huge.
There are very simple solutions for other languages, too. E.g. use PHP's built in web server on 8080 php -S localhost:8080.
Now fire your request (in Insomnia) and you should be prompted with the login:
Log in with your email and password and confirm the consent screen (should contain your chosen scopes).
Go back to your terminal and check the output. If you logged the whole thing scroll down (e.g. pgdown in less) until you see a line with code=4/....
Copy that code; it is your authorization code that you'll want to exchange for an access and refresh token. Don't copy too much - if there's an ampersand & do not copy it or anything after. & delimits query parameters. We just want the code.
Now set up a HTTP POST request pointing to https://www.googleapis.com/oauth2/v4/token as form URL encoded. In Insomnia you can just click that - in other tools you might have to set the header yourself to Content-Type: application/x-www-form-urlencoded.
Add the following parameters:
code=<the authorization code from the last step>
client_id=<your client ID again>
client_secret=<your client secret from the OAuth credentials>
redirect_uri=http://localhost:8080
grant_type=authorization_code
Again, make sure that the encoding is correct.
Fire your request and check the output from your server. In the response you should see a JSON object:
{
"access_token": "xxxx",
"expires_in": 3600,
"refresh_token": "1/xxxx",
"scope": "https://www.googleapis.com/auth/drive.file",
"token_type": "Bearer"
}
You can use the access_token right away but it'll only be valid for one hour. Note the refresh token. This is the one you can always* exchange for a new access token.
* You will have to repeat the procedure if the user changes his password, revokes access, is inactive for 6 months etc.
Happy OAuthing!

Resources