Azure Bot - Directline token generation from html page hiding secret - azure-bot-service

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.

Related

How to refresh id-token using #microsoft/teamsfx

I created a Teams tab application by customizing the SSO react app sample from the Teams toolkit. The application redirects the user to our website (inside one of the tabs). I can grab the id-token in react (teamsfx.getCredentials().getToken("")) and pass it to our web application via a query parameter.
This id-token is validated and then passed around to various microservices that comprise our backend.
This part works well, but then, we had the need to refresh the token. So, we decided for the web application (written in Angular) to fetch the token using #microsoft/teamsfx and #microsoft/teams-js npm packages.
While I am not certain if that is the way to go, when I execute the following code inside an angular service, it throws the "SDK initialization timed out" error.
try {
const teamsFx: TeamsFx = new TeamsFx(IdentityType.User, {
"clientId": "ee89fb47-a378-4096-b893-**********",
"objectId": "df568fe9-3d33-4b22-94fc-**********",
"oauth2PermissionScopeId": "4ce5bb24-585a-40d3-9891-************",
"tenantId": "5d65ee67-1073-4979-884c-**************",
"oauthHost": "https://login.microsoftonline.com",
"oauthAuthority": "https://login.microsoftonline.com/5d65ee67-1073-4979-884c-****************",
"applicationIdUris": "api://localhost/ee89fb47-a378-4096-b893-***************",
"frontendEndpoint": "https://localhost",
"initiateLoginEndpoint": "https://localhost:8101"
});
const creds = await teamsFx.getCredential().getToken('https://graph.microsoft.com/User.Read');
const token = creds?.token;
console.log("New Token: ", token);
const expirationTimestamp = creds?.expiresOnTimestamp;
this.scheduleRefresh(expirationTimestamp);
this.tokenRefreshed.next({ token: token, expiresOnTimestamp: expirationTimestamp });
}
catch (error) {
console.error("Error in getNewTeamsToken(): ", error);
}
Am I missing anything here, or is the approach itself wrong? Please advise.
Teams is basically just using MSAL under the covers (with a bit of other stuff thrown in I think) to get the token. If you want to be able to authenticate your user outside of Teams, in a separate web app, you can simply use MSAL yourself (something like this I'd guess: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-angular-auth-code).
That would mean, essentially, that you have a tab web app, doing Teams SSO and then a separate standalone Angular app doing direct MSAL auth. Does you tab app actually do anything? If not, and you're only using it to redirect, you can instead combine these into a single app, and detect in the app whether you're in Teams or not - if you are, do SSO using TeamsFX. If not, do MSAL login directly. This link shows how to detect if your inside of Teams or not.
If you want to continue with separate apps that's fine, but I absolutely would not pass the token as a QueryString parameter - that is extremely dangerous. First of all, tokens are not meant to be passed like this at all, as they could be intercepted. Secondly, passing them on Querystring means that they are entirely open - anything inbetween can sniff the address and lift out your token (if it was a POST payload with httpS, for instance, at least the 'S' would be encrypting the token between browser and server).

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

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.

Using Azure Active Directory token in ASP.Net Core Web API with UseJwtBearerAuthenticiation

I have a native client application written in Ionic Framework 3. I have a Web API written in ASP.NET Core 1.1. I want to use Azure Active Directory to manage access to the Web API.
I have registered two applications with Azure Active Directory: Mobile App and Web API. The Mobile App has the required permission of granting access to the Web API. Below are screen shots of the permissions from our Azure Admin Portal:
Mobile App Permissions
This is configured as a Native App in AAD. I have an Application ID and an Object ID given by AAD. Additionally, I added an arbitrary Redirect URI, which I thought based on several tutorials did not need to resolve, that URI is http://mobileCRMApp. Looking at the Properties in AAD, the Home page URL is blank and the Logout URL is blank.
API Permissions
BOLD UPDATED 10/03/2017:
This is configured as a Web App/API in AAD. I have an Application ID and an Object ID given by AAD. Additionally, I set both the Home Page URL and the App ID URI to match the root of my Web API (https://crm.mycompany.com).
My Ionic client application successfully authenticates against AAD roughly in the following way:
authenticate(userID, authCompletedCallback) : any {
let parent = this;
//this.context = new AuthenticationContext(this.config.authority);
let context = this.msAdal.createAuthenticationContext("https://login.microsoftonline.com/myTenantId");
console.log(context);
context.acquireTokenAsync(parent.config.resourceUri, parent.config.clientId, parent.config.redirectUri, userID, "")
.then(authCompletedCallback)
.catch((e: any) => console.log('Authentication failed', e));
}
The login process goes fine in the app, and the callback receives a token, and I can translate its payload using jwt.io into roughly the following:
{
"aud": "https://crm.mycompany.com/",
"iss": "https://sts.windows.net/someID/",
"iat": 1506539211,
"nbf": 1506539211,
"exp": 1506543111,
"acr": "1",
"aio": "someOtherID",
"amr": [
"pwd"
],
"appid": "appID",
"appidacr": "0",
"e_exp": 262800,
"family_name": "Walter",
"given_name": "Philip",
"ipaddr": "someAddress",
"name": "Philip Walter",
"oid": "someOtherID",
"onprem_sid": "someOtherID",
"puid": "stuff",
"scp": "user_impersonation",
"sub": "e_X7WlAoVS2vzXm1pr3kcDOrET7czcC0f8-YRU_2DJ8",
"tenant_region_scope": "NA",
"tid": "ourTenantID",
"unique_name": "pwalter#advtis.com",
"upn": "pwalter#advtis.com",
"uti": "RLvLlibQHESwmujVBBdlAA",
"ver": "1.0"
}
So then I take the token and send it along with an http request to the API from the Ionic client app, roughly like so:
let headers = new Headers();
headers.append('Authorization', 'Bearer ' + this.authToken.accessToken);
let options = new RequestOptions({ headers : headers });
this.data.http.get(url, options).map(res => res.json()).subscribe((data) => {
console.log(data);
});
The API then has the following in Startup.cs
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = Configuration["Authentication: AzureAd:AADInstance"] + Configuration["Authentication: AzureAd:TenantId"],
Audience = Configuration["Authentication: AzureAd:Audience"]
});
So, I can log in through AAD in the client app, and I receive a token, but I still get a 401 unauthorized response from the web api when I send a request to a route with the [Authorize] tag above it.
I am obviously doing something wrong in configuring the API or the permissions. I put this together using several different tutorials, because I could not find anything that specifically addressed my use case. Any ideas as to what I'm doing wrong, or how I might troubleshoot?
Why does your audience say "aud": "https://graph.windows.net"?
It should say "aud": "https://yourWebApi.azurewebsites.net" or something similar.
The token you included above is only good for the Graph API, any other Azure AD protected resource will refuse it, including your Web API, since it expects the audience to match self.
parent.config.resourceUri is probably where you source the desired audience from:
context.acquireTokenAsync(
parent.config.resourceUri,
...
According to RFC 7519:
The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case- sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.
Sometimes it's called resource, sometimes audience, sometimes aud... it is what it is :)

Google Oauth Error: redirect_uri_mismatch

I'm trying to use google Oauth 2 to authenticate with google calendar API for a web server running on AWS EC2.
When I generated the credentials I selected 'OAuth Client ID' and then 'Web Application'. For the Authorised redirect URIs I have entered:
http://ec2-XX-XX-XX-XXX.eu-west-1.compute.amazonaws.com
(I've blanked out the IP of my EC2 instance). I have checked this is the correct URL that I want the callback to go to.
The link that is generated in the server logs is of the form:
https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=XXXXXXXXXXXX-XXXXXXXXXXXXXX.apps.googleusercontent.com&redirect_uri=http://localhost:47258/Callback&response_type=code&scope=https://www.googleapis.com/auth/calendar.readonly
When I follow the link I get the error
'Error: redirect_uri_mismatch'.
I've read this SO question and have checked that I am using HTTP and there is no trialing '/'
I suspect that the URL generated should not have 'localhost' in it but I've reset the client_secret.json several times and each time I restart tomcat with the new client secret I still get a link with localhost but just over a different port.
Locally, I had selected Credentials type of 'other' previously and was not given an option for the Authorised redirect URI. I did try this for the EC2 instance but this won't give me the control I want over the redirect URI and sends the redirect over localhost.
Google throws redirect_uri_mismatch when the uri (including ports) supplied with the request doesn't match the one registered with the application.
Make sure you registered the Authorised redirect URIs and Authorised JavaScript origins on the web console correctly.
This is a sample configuration that works for me.
In case you are seeing this error while making API call from your server to get tokens.
Short Answer 👇 - What solved my problem
use string postmessage in place of actual redirectUri that you configured on cloud console.
Here is my initilization of OAuth2 client that worked for me.
// import {Auth, google} from 'googleapis`;
const clientID = process.env.GOOGLE_OAUTH_CLIENT_ID;
const clientSecret = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
oauthClient = new google.auth.OAuth2(clientID,clientSecret,'postmessage');
My Case
On the frontend, I am using react to prompt the user for login with google with the authentication-code flow. On success, this returns code in the payload that needs to be posted to the google API server to get token - Access Token, Refresh Token, ID Token etc.
I am using googleapis package on my server. Here is how I am retrieving user info from google
// import {Auth, google} from 'googleapis`;
const clientID = process.env.GOOGLE_OAUTH_CLIENT_ID;
const clientSecret = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
oauthClient = new google.auth.OAuth2(clientID,clientSecret,'postmessage');
/*
get tokens from google to make api calls on behalf of user.
#param: code -> code posted to backend from the frontend after the user successfully grant access from consent screen
*/
const handleGoogleAuth = (code: string) => {
oauthClient.getToken(code, async (err, tokens: Auth.Credentials) {
if (err) throw new Error()
// get user information
const tokenInfo = await oauthClient.verifyIdToken({
idToken: tokens.id_token
});
const {email, given_name, family_name, email} = tokenInfo.getPayload();
// do whatever you want to do with user informaton
}
}
When creating a Oath client ID, DO NOT select web application, Select "Other". This way, the Redirect URI is not required.

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