How to handle DirectLine connection errors - javascript

we run a bot using bot framework and connect to it from our website using direct line in JS. We get a token from a custom API endpoint and we store the token in sessionStorage. Then we connect to the bot using
directLine = await window.WebChat.createDirectLine({
token,
conversationId,
watermark: "0"
});
Everything is working fine, but when I leave the page open for too long the token in the sessionStorage expires. A page refresh or navigating to a different page causes a 403 error inside the createDirectLine method. Resulting in a chat bot that can't connect for as long as the sessionStorage holds that token. This behavior is not a surprise to me, but I don't know how to handle this.
What I want is to simply clear the sessionStorge, request a new token and start a new conversation when this happens. But I don't know how to do that. How do I get the 403 error from the createDirectLine method? Or is there a way to validate the token upfront?
I already tried putting a try/catch block around the createDirectLine method, but the 403 error did not show up in the catch.
Thanks in advance!

This solution is only to address the 403 error from occurring since the token expires (in 30 minutes I think). A better solution is to store the conversationId with the token and get a new token with that. Check official bot service documentation for that.
// to shorten code, we store in sessionStorage as separate items.
const expirationDuration = 1000 * 60 * 30; // 30 minutes
const currentTime = new Date().getTime();
const timeTokenStored = sessionStorage.getItem("timeTokenStored") || currentTime;
// if token is stored over 30 minutes ago, ignore it and get a new one. Otherwise, use it.
if ((currentTime - timeTokenStored) > expirationDuration) {
const res = await fetch('https://<yourTokenEndpoint>', { method: 'POST' });
const { token } = await res.json();}
const currentTime = new Date().getTime();
sessionStorage.setItem("timeTokenStored", currentTime);
sessionStorage.setItem('token', token);
else {
const token = sessionStorage.getItem("token")
}
While your're at it, you might as well store it in localStorage. This way, your bot will follow the user.

I have found the solution. We can check that a token is valid by refreshing it. If refreshing causes an error, the token is not valid anymore. If refreshing succeeds, the toke will be valid for another hour.
So we added (resusable) a function to our back end to refresh the token using https://directline.botframework.com/v3/directline/tokens/refresh.
And we changed the front end code to call our new refresh function.
Front end code:
// Gets a new token from the cloud.
async function requestToken() {
if (!sessionStorage['webchatToken']) {
const res = await fetch('https://' + serviceName + '.azurewebsites.net/api/token');
// If the request was succesfull, store the token and userId.
if (res.status == 200) {
const jsonResult = await res.json();
sessionStorage['webchatToken'] = jsonResult.token;
sessionStorage['webchatUserId'] = jsonResult.userId;
console.log(`Got token from cloud`);
// refresh the token every 15 minutes.
setTimeout(() => {
refreshToken();
}, 60000 * 15); // 15 minutes
}
// If the request was not succesfull, retry.
else {
console.log(`Tried to get token, but goterror ` + res.status + `. Retrying.`);
await requestToken();
}
}
// If there is already a token in storage, refresh the existing one instead of requesting a new one.
else {
console.log(`Got token from sessionStorage`);
await refreshToken();
}
}
// Refreshes an existing token so it doesn't expire.
async function refreshToken() {
// Refresh the token if it exists in storage.
if (sessionStorage['webchatToken']) {
const res = await fetch('https://' + serviceName + '.azurewebsites.net/api/token/refresh?token=' + sessionStorage['webchatToken'],
{
method: 'POST'
});
// If refresh was succesfull we are done.
if (res.status == 200) {
console.log(`Refreshed token`);
}
// If refresh was not succesfull, clear the token from storage and request a new one. The token is probably expired.
else {
console.log(`Tried to refresh token, but got error ` + res.status + `. Requesting new token.`);
sessionStorage.clear();
await requestToken();
}
}
// If there is no token in storage, request a new token.
else {
console.log(`Tried to refresh token, but token is not defined. Requesting new token.`);
sessionStorage.clear();
await requestToken();
}
}
Back end code:
[HttpGet]
[Route("api/token")]
public async Task<ObjectResult> GetToken()
{
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Post,
$"https://directline.botframework.com/v3/directline/tokens/generate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _configuration.DirectLineKey);
var userId = $"dl_{Guid.NewGuid()}";
request.Content = new StringContent(
JsonConvert.SerializeObject(new { User = new { Id = userId } }),
Encoding.UTF8,
"application/json");
var response = await client.SendAsync(request);
string token = String.Empty;
int expiresIn = 0;
if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
expiresIn = JsonConvert.DeserializeObject<DirectLineToken>(body).expires_in;
}
return Ok(new { token, userId, expiresIn });
}
[HttpPost]
[Route("api/token/refresh/")]
public async Task<ObjectResult> RefreshToken(string token)
{
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(
HttpMethod.Post,
$"https://directline.botframework.com/v3/directline/tokens/refresh");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.SendAsync(request);
token = String.Empty;
int expiresIn = 0;
if (response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
expiresIn = JsonConvert.DeserializeObject<DirectLineToken>(body).expires_in;
}
if (string.IsNullOrEmpty(token))
return Problem("Token incorrect");
return Ok(new { token, expiresIn });
}
I hope posting this may be useful to somebody.

Related

Client receive different http code from what the server had response

I had this weird going on.
I'm using Jersey for API end-point, HTML and javascript front end. When I authenticate user, server will response 303 to tell the client the user is authenticated and to redirect to the said page (based on user group which the user belongs to), which I put it in Location header.
But when my server response with code 303, the client (html with pure javascript in this case) receive code of 200. But if I change the code to something else, let say 401, the client receive it correctly. This happened only if server response with code 303.
Why I return 303? I'm not pretty sure myself, thought it is the right way to do it, I might just return 200, but I try to do it the proper way as much as I know and can. But that is for another time, suggestion are welcome.
And even when I try to receive the Location from header and token from cookies, it return null. As if something happened to the response from server to client. I don't have anything that change response.
Looking at the application log, everything went fine, nothing miss behave.
It was working fine, but suddenly this weird behavior happened. I already clear browser cache, clean build and deploy the app, restart tomcat, and even restart my dev machine, nothing solve it.
I not found anything related with google. My way of code things might not the right way as I'm quite new with REST.
My javascript:
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if(xhr.status === 200) {
console.log("Login API return 200");
console.log("Token: " + xhr.getResponseHeader("Token"));
} else if (xhr.status === 303) {
console.log("Status 303 with response text " + xhr.responseText);
console.log("Status 303 with redirect header of " + xhr.getResponseHeader("redirect"));
// redirect user to the page
window.location.href = xhr.getResponseHeader("Location");
} else if (xhr.status === 401) {
console.log('Unauthorized access to server.');
document.getElementById("expired").style.display = "none";
document.getElementById("credential").style.display = "block";
} else {
console.log("Returned status is " + xhr.status);
console.log("Response text is " + xhr.responseText);
}
}
};
xhr.open('POST', URL + "/api/v1/users/login");
xhr.setRequestHeader("authorization", authHeader);
xhr.send();
My java:
#POST
#Path("/login")
#Produces(MediaType.TEXT_PLAIN)
public Response authUser(#Context HttpHeaders httpHeaders,
#Context HttpServletRequest request) {
log.debug("Server name : " + request.getServerName());
String authHeader = httpHeaders.getRequestHeader("authorization").get(0);
String encodeAuth = authHeader.substring(authHeader.indexOf(' ') + 1);
String decodeAuth = new String(Base64.getDecoder().decode(encodeAuth));
String username = decodeAuth.substring(0, decodeAuth.indexOf(':'));
String password = decodeAuth.substring(decodeAuth.indexOf(':') + 1);
User user = new User(username);
log.debug("Username : " + username);
log.debug("Password : " + password);
// Check user
if (!user.login(username,password)) {
// This is a response for the ajax for unauthorized login
log.warn("Can not authenticate user, either wrong username or password.");
// Return Unauthorized response
return Response
.status(401)
.entity("Unauthorized")
.build();
}
log.debug("Authentication success. Proceed with token and cookie creation");
// Create token
String token = TokenStore.getInstance().putToken(username);
// Get user's group
String userGroup = TokenStore.getInstance().getGroup(token);
log.debug("Token created for the user is " + token);
// Create cookie
NewCookie cookie = new NewCookie("Token",token,"/app/", request.getServerName(),"User token",1800,false);
/* Create redirect URL.
* uriInfo.getBaseUriBuilder() will return http://<host>:<port>/app/api/ which is not
* desired in this scenario, the below is to eliminate /api/ from the URI
* to get only http://<host>:<port>/app
*/
String uri = uriInfo.getBaseUriBuilder().toString();
// nthLastIndexOf(2, "/", uri) is a helper method to get the nth occurrence of a character in a string, from the last character.
int secondLast = nthLastIndexOf(2, "/", uri);
String base = uri.substring(0,secondLast);
String redirect = "";
if(null != userGroup) switch (userGroup) {
case "groupA":
redirect = base + "/pageA";
break;
case "groupB":
redirect = base + "/pageB";
break;
case "groupC":
redirect = base + "/pageC";
break;
default:
break;
}
URI re = null;
log.debug("Created URI for redirect is " + redirect);
try {
re = new URI(redirect);
} catch (URISyntaxException syntax) {
log.error("Cannot convert string to URI", syntax);
} catch (Exception e) {
log.error("Cannot convert string to URI", e);
}
log.debug("Return response 303.");
// Return redirect response
return Response
.status(303)
.entity(token)
.header("Location", redirect)
.cookie(cookie)
.build();
}

Javascript Caching API Issues

I am using worker to do my caching task based on device type because the theme I am using doesn't have a separate mobile cache and also Cloudflare doesn't have device type caching at the server's edge, Therefor I used a worker to do this task. With the normal GET request at first, it shows cache status dynamic after a couple of reloads it shows to hit.
There is also a POST method in the worker code so when the POST request is made then it should make also a GET request so the cache for that product is saved. The problem I am facing with the POST method whenever I publish a product the cache is not re-validated and therefore I am getting an old cache. Even if I purge cache it doesn't re-validate cache or show expire cache, It shows to hit.
I know I am not doing it wrong, I would be very thankful if you can help me, below is the code snippet I am using.
Thank you so much.
async function run(event) {
const { request } = event;
const cache = caches.default;
// Read the user agent of the request
const ua = request.headers.get('user-agent');
let uaValue;
if (ua.match(/mobile/i)) {
uaValue = 'mobile';
} else {
uaValue = 'desktop';
}
// Construct a new response object which distinguishes the cache key by device
// type.
const url = new URL(request.url);
url.searchParams.set('ua', uaValue);
const newRequest = new Request(url, request);
let response = await cache.match(newRequest);
if (!response) {
// Use the original request object when fetching the response from the
// server to avoid passing on the query parameters to our backend.
response = await fetch(request);
response = new Response(response.body, response)
response.headers.append("Cache-Control", "max-age=31536000")
// Store the cached response with our extended query parameters.
event.waitUntil(cache.put(newRequest, response.clone()));
}
return response;
}
async function sha256(message) {
// encode as UTF-8
const msgBuffer = new TextEncoder().encode(message)
// hash the message
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer)
// convert ArrayBuffer to Array
const hashArray = Array.from(new Uint8Array(hashBuffer))
// convert bytes to hex string
const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("")
return hashHex
}
async function handlePostRequest(event) {
const request = event.request
const body = await request.clone().text()
const hash = await sha256(body)
const cacheUrl = new URL(request.url)
// Store the URL in cache by prepending the body's hash
cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash
// Convert to a GET to be able to cache
const cacheKey = new Request(cacheUrl.toString(), {
headers: request.headers,
method: "GET",
})
const cache = caches.default
// Find the cache key in the cache
let response = await cache.match(cacheKey)
// Otherwise, fetch response to POST request from origin
if (!response) {
response = await fetch(request)
event.waitUntil(cache.put(cacheKey, response.clone()))
}
return response
}
addEventListener("fetch", event => {
try {
const request = event.request
if (request.method.toUpperCase() === "POST")
return event.respondWith(handlePostRequest(event))
return event.respondWith(run(event))
} catch (e) {
return event.respondWith(new Response("Error thrown " + e.message))
}
})

How to enable CORS in an Azure App Registration when used in an OAuth Authorization Flow with PKCE?

I have a pure Javascript app which attempts to get an access token from Azure using OAuth Authorization Flow with PKCE.
The app is not hosted in Azure. I only use Azure as an OAuth Authorization Server.
//Based on: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead
var config = {
client_id: "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize",
token_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token",
requested_scopes: "openid api://{tenant-id}/user_impersonation"
};
// PKCE HELPER FUNCTIONS
// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
const hashed = await sha256(v);
return base64urlencode(hashed);
}
// Parse a query string into an object
function parseQueryString(string) {
if (string == "") { return {}; }
var segments = string.split("&").map(s => s.split("="));
var queryString = {};
segments.forEach(s => queryString[s[0]] = s[1]);
return queryString;
}
// Make a POST request and parse the response as JSON
function sendPostRequest(url, params, success, error) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.onload = function () {
var body = {};
try {
body = JSON.parse(request.response);
} catch (e) { }
if (request.status == 200) {
success(request, body);
} else {
error(request, body);
}
}
request.onerror = function () {
error(request, {});
}
var body = Object.keys(params).map(key => key + '=' + params[key]).join('&');
request.send(body);
}
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = 'Hello'+ 'webpack';
element.classList.add('hello');
return element;
}
(async function () {
document.body.appendChild(component());
const isAuthenticating = JSON.parse(window.localStorage.getItem('IsAuthenticating'));
console.log('init -> isAuthenticating', isAuthenticating);
if (!isAuthenticating) {
window.localStorage.setItem('IsAuthenticating', JSON.stringify(true));
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);
// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);
// Build the authorization URL
var url = config.authorization_endpoint
+ "?response_type=code"
+ "&client_id=" + encodeURIComponent(config.client_id)
+ "&state=" + encodeURIComponent(state)
+ "&scope=" + encodeURIComponent(config.requested_scopes)
+ "&redirect_uri=" + encodeURIComponent(config.redirect_uri)
+ "&code_challenge=" + encodeURIComponent(code_challenge)
+ "&code_challenge_method=S256"
;
// Redirect to the authorization server
window.location = url;
} else {
// Handle the redirect back from the authorization server and
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));
console.log('queryString', q);
// Check if the server returned an error string
if (q.error) {
alert("Error returned from authorization server: " + q.error);
document.getElementById("error_details").innerText = q.error + "\n\n" + q.error_description;
document.getElementById("error").classList = "";
}
// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Exchange the authorization code for an access token
// !!!!!!! This POST fails because of CORS policy.
sendPostRequest(config.token_endpoint, {
grant_type: "authorization_code",
code: q.code,
client_id: config.client_id,
redirect_uri: config.redirect_uri,
code_verifier: localStorage.getItem("pkce_code_verifier")
}, function (request, body) {
// Initialize your application now that you have an access token.
// Here we just display it in the browser.
document.getElementById("access_token").innerText = body.access_token;
document.getElementById("start").classList = "hidden";
document.getElementById("token").classList = "";
// Replace the history entry to remove the auth code from the browser address bar
window.history.replaceState({}, null, "/");
}, function (request, error) {
// This could be an error response from the OAuth server, or an error because the
// request failed such as if the OAuth server doesn't allow CORS requests
document.getElementById("error_details").innerText = error.error + "\n\n" + error.error_description;
document.getElementById("error").classList = "";
});
}
// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
}
}
}());
In Azure I only have an App registration (not an app service).
Azure App Registration
The first step to get the authorization code works.
But the POST to get the access token fails. (picture from here)
OAuth Authorization Code Flow with PKCE
Access to XMLHttpRequest at
'https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token' from
origin 'http://localhost:8080' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Where in Azure do I configure the CORS policy for an App Registration?
Okay, after days of banging my head against the stupidity of Azure's implementation I stumbled upon a little hidden nugget of information here: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser#prerequisites
If you change the type of the redirectUri in the manifest from 'Web' to 'Spa' it gives me back an access token! We're in business!
It breaks the UI in Azure, but so be it.
You should define the internal url with your local host address.
https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-understand-cors-issues
When I first posted, the Azure AD token endpoint did not allow CORS requests from browsers to the token endpoint, but it does now. Some Azure AD peculiarities around scopes and token validation are explained in these posts and code in case useful:
Code Sample
Blog Post

Verify Token generated in C# with System.IdentityModel.Tokens.Jwt

I have created a token like this in my web api with C#.
private const string Secret = "someSecretKey";
public static string GenerateToken(AuthModel user, int expireMinutes = 20)
{
var symmetricKey = Convert.FromBase64String(Secret);
var tokenHandler = new JwtSecurityTokenHandler();
var now = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, ((Roles)user.RoleId).ToString()),
new Claim("guid",user.Guid)
}),
Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(symmetricKey), SecurityAlgorithms.HmacSha256Signature)
};
var stoken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(stoken);
return token;
}
and when im usin that API for reactjs app i get the token but cant verify it with same secret key.
Im getting error INVALID SIGNATURE.
Im using jsonwebtoken npm package,
import jwt from 'jsonwebtoken';
jwt.verify(token, keys.jwtSecret, async (err) => {
if (err) {
//console.log('Token expired at: ', err.expiredAt)
console.log("error", err)
}
else {
dispatch(login(token));
}
});
i never hit that dispatch(login(token)). I'm using this to check if token saved in localStorage is still valid to keep user signed in.
Any help is appreciated.
I've found solution. Couldn't just push secretKey in jwt.verify(token,secretKey); That doesn't work because some base64 encoding/decoding algorithms. What i had to do is first to make a Buffer from my secret like:
const secret = new Buffer("myTokeSecretString", "base64");
and then pass that secret to verify method and it works.

Validating Firebase tokens in PHP when sent from JavaScript auth

Working on a project that involves a Firebase-utilizing JavaScript web app that reaches out to a PHP file carrying protected functionality.
In order to do this I get a (JWT) token by calling:
firebase.auth().currentUser.getToken(true)
The full function being:
firebase.auth().currentUser.getToken(true).then(function(idToken) {
var uid = firebase.auth().currentUser.uid;
var http = new XMLHttpRequest();
var url = "http://localhost/jwt.php";
var params = "token=" + idToken + "&uid=" + uid;
http.open("POST", url, true);
//Send the proper header information along with the request
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.onreadystatechange = function() {//Call a function when the state changes.
if(http.readyState == 4 && http.status == 200) {
alert(http.responseText);
}
}
http.send(params);
console.log("TOKEN: " + idToken);
}).catch(function(error) {
// Handle error
});
On the PHP side I'm validating the token using the lcobucci/jwt library.
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Keychain;
use Lcobucci\JWT\Signer\Rsa\Sha256;
$data = new ValidationData();
$data->setIssuer('https://securetoken.google.com/<Project ID>');
$signer = new Sha256();
$keychain = new Keychain();
if($_POST["token"]) {
$token = (new Parser())->parse((string) $_POST["token"]);
$token->getHeaders(); // Retrieves the token header
$token->getClaims(); // Retrieves the token claims
$kid = $token->getHeader('kid');
$iat = $token->getClaim('iat');
//Grab Google keys
$json_url = file_get_contents('https://www.googleapis.com/robot/v1/metadata/x509/securetoken#system.gserviceaccount.com');
$json = json_decode($json_url, true);
$public_key = $json[$kid]; // Matches kid from header to private key provided by Google
try {
$isTokenValid = $token->verify($signer, $public_key); // Verify token
} catch (Exception $e) {
$isTokenValid = false;
}
if($isTokenValid) {
echo "Valid"; // Add protected functionality here
} else {
echo "Invalid";
}
}
My question is: is this secure?
Yes, verifying the token signature like this is secure.
This will prove that the token content was not modified and signed with a key from Google.
You can learn more about JWT here: https://jwt.io/introduction/
Additionally you can validate the token
$token->validate($data);
This will validate the the issuer (iss claim) and expiration time of the token (exp claim)
https://github.com/lcobucci/jwt/blob/3.2/README.md

Categories

Resources