invalid_request from getToken in Javascript from Node.js - javascript

I have the following code in a node.js server application
app.get('/AuthorizeGoogle.html',function(req,res) {
var auth = new googleapis.OAuth2Client(config.google_login.client_id, config.google_login.client_secret, config.google_login.redirect_uri);
var queryData = url.parse(req.url,true).query;
var code = encodeURIComponent(queryData.code);
console.log('Authorization Request recieved ');;
console.log('Retrieiving token');
auth.getToken(code,function(err,tokens) {
console.log('Retrievied token ');
console.log(tokens);
console.log('**** ERROR ****');
console.log(err);
console.log('Calling setCredentials');
auth.setCredentials(tokens);
console.log('*****FINISHED!!!!!');
});
res.send('Authorization recieved ' + queryData.code)
});
This is called when Google returns and the user has authorised access to their Google account.
I get a code. However when auth.getToken() I am getting invalid_request. I have done this successfully in C#, but I am now moving to more open source tools hence moving the project to Node.js
Thanks in advance
OK - I looked again at the page suggested and did some refactoring of my code and that worked. I think what may have been the problem was the Url used to get the token in the first place.
I was already initialising the oauth2Client
var OAuth2Client = googleapis.OAuth2Client;
var oauth2Client;
oauth2Client = new OAuth2Client(config.google_login.client_id, config.google_login.client_secret, config.google_login.redirect_uri);
The required Client Id, secret and redirect Url have been defined in a configuration file
So first all I changed the way I was generating that url. First off I set the Url to Login to Google to GoogleLogin.html which executes the following when the server receives a request for this page.
app.get('/GoogleLogin.html',function(req,res) {
var scopes = "";
// retrieve google scopes
scopes += config.google_login.scopes.baseFeeds + " "
scopes += config.google_login.scopes.calendar + " "
scopes += config.google_login.scopes.drive + " "
scopes += config.google_login.scopes.driveMetadata + " "
scopes += config.google_login.scopes.profile + " "
scopes += config.google_login.scopes.email + " "
scopes += config.google_login.scopes.tasks
var url = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: scopes
});
res.writeHead(302, {location: url});
res.end();
});
This first building a string of the scopes then generating the Authorization Url before redirecting to the generated url
When Google redirects back to the site the following is executed
app.get('/AuthorizeGoogle.html',function(req,res) {
// var auth = new googleapis.OAuth2Client(config.google_login.client_id,config.google_login.client_secret, config.google_login.redirect_uri);
var queryData = url.parse(req.url,true).query;
var code = queryData.code;
console.log('Authorization Request recieved ');;
console.log('Retrieving token');
oauth2Client.getToken(code,function(err,tokens) {
console.log('Retrieved token ');
console.log(tokens);
console.log('**** ERROR ****');
console.log(err);
console.log('Calling setCredentials');
oauth2Client.setCredentials(tokens);
console.log('*****FINISHED!!!!!');
});
res.send('Authorization recieved ' + queryData.code)
});
And this is now returning the Token and Refresh token correctly. Since it is the same code the only thing I can was wrong was the original call to Google.

I think your best bet would be to research what is done in the following github example: https://github.com/google/google-api-nodejs-client/blob/master/examples/oauth2.js

Related

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

ADAL JS not attaching user token while invoking WebApi

I am using ADAL JS for authenticating the users against Azure AD. And as I am new to ADAL JS, I started reading with following articles, which I find very informative:
Introducing ADAL JS v1
ADAL JavaScript and AngularJS – Deep Dive
After reading the articles, I had the impression that ADAL JS intercepts the service calls and if the service url is registered as one of the endpoint in AuthenticationContext configuration, it attaches the JWT token as Authentication Bearer information.
However, I found the same is not happening in my case. And after some digging, it seemed to me that it is only possible, if adal-angular counter part is also used, which I am not using currently, simply because my web application is not based on Angular.
Please let me know if my understanding is correct or not. If I need to add the bearer information explicitly, the same can be done, but I am more concerned whether I am missing some out-of-the-box facility or not.
Additional Details: My present configuration looks like following:
private endpoints: any = {
"https://myhost/api": "here_goes_client_id"
}
...
private config: any;
private authContext: any = undefined;
....
this.config = {
tenant: "my_tenant.onmicrosoft.com",
clientId: "client_id_of_app_in_tenant_ad",
postLogoutRedirectUri: window.location.origin,
cacheLocation: "sessionStorage",
endpoints: this.endpoints
};
this.authContext = new (window["AuthenticationContext"])(this.config);
Also on server-side (WebApi), Authentication configuration (Startup.Auth) is as follows:
public void ConfigureOAuth(IAppBuilder app, HttpConfiguration httpConfig)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = "my_tenant.onmicrosoft.com",
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = "client_id_of_app_in_tenant_ad"
}
});
}
However, the Authorization is always null in request.Headers.
UPDATE: It seems that the same applies for auto-renewal of tokens as well; when used in conjunction with adal-angular, the renewal of token works seamlessly by calling AuthenticationContext.acquireToken(resource, callback) under the hood. Please correct me if I am wrong.
After reading the articles, I had the impression that ADAL JS intercepts the service calls and if the service url is registered as one of the endpoint in AuthenticationContext configuration, it attaches the JWT token as Authentication Bearer information.
This will work only if your application is angular based. As you mentioned, the logic for this lives in adal-angular.
If, however, you want to stick to pure JS, you will not get the automatic "get-access-token-and-attach-it-to-header" support. You can use acquireToken(resource, callback api to get a token for the endpoint. But you will have to do some work in the controller that is sending the request to the api.
This might give you some idea: https://github.com/Azure-Samples/active-directory-javascript-singlepageapp-dotnet-webapi/blob/master/TodoSPA/App/Scripts/Ctrls/todoListCtrl.js. This sample does not uses angular.
ADAL.JS is incompatible with v2.0 implicit flow. I could not get it working since I set my project up recently and don't think projects are backwards compatible.
This was very confusing and took me a long time to figure out that I was mixing up the versions, and can't use ADAL.JS with v2.0. Once I removed it, things went much smoother, just did a couple of XHR requests and a popup window, no magic actually required!
Here is code for v2:
function testNoADAL() {
var clientId = "..guid..";
var redirectUrl = "..your one.."
var authServer = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?";
var responseType = "token";
var stateParam = Math.random() * new Date().getTime();
var authUrl = authServer +
"response_type=" + encodeURI(responseType) +
"&client_id=" + encodeURI(clientId) +
"&scope=" + encodeURI("https://outlook.office.com/Mail.ReadWrite") +
"&redirect_uri=" + encodeURI(redirectUrl) +
"&state=" + stateParam;
var popupWindow = window.open(authUrl, "Login", 'width=' + 300 + ', height=' + 600 + ', top=' + 10 + ', left=' + 10 + ',location=no,toolbar=yes');
if (popupWindow.focus) {
popupWindow.focus();
}
}
Note: redirectUrl will appear in popup window, needs to have code in it to pass location hash, such as this:
<script>window.opener.processMicrosoftAuthResultUrl(location.hash);window.close();</script>
function processMicrosoftAuthResultUrl(hash) {
if (hash.indexOf("#") == 0) {
hash = hash.substr(1);
}
var obj = getUrlParameters(hash);
if (obj.error) {
if (obj.error == "invalid_resource") {
errorDialog("Your Office 365 needs to be configured to enable access to Outlook Mail.");
} else {
errorDialog("ADAL: " + obj.error_description);
}
} else {
if (obj.access_token) {
console.log("ADAL got access token!");
var token = obj.access_token;
var url = "https://outlook.office.com/api/v2.0/me/MailFolders/Inbox/messages";
$.ajax({
type: "GET",
url: url,
headers: {
'Authorization': 'Bearer ' + token,
},
}).done(function (data) {
console.log("got data!", data);
var message = "Your latest email is: " + data.value[0].Subject + " from " + data.value[0].From.EmailAddress.Name+ " on " + df_FmtDateTime(new Date(data.value[0].ReceivedDateTime));
alertDialog(message);
}).fail(function () {
console.error('Error getting todo list data')
});
}
}
}
function getUrlParameters(url) {
// get querystring and turn it into an object
if (!url) return {};
if (url.indexOf("?") > -1) {
url = url.split("?")[1];
}
if (url.indexOf("#") > -1) {
url = url.split("#")[0];
}
if (!url) return {};
url = url.split('&')
var b = {};
for (var i = 0; i < url.length; ++i) {
var p = url[i].split('=', 2);
if (p.length == 1) {
b[p[0]] = "";
} else {
b[decodeURIComponent(p[0])] = decodeURIComponent(p[1].replace(/\+/g, " "));
}
}
return b;
}

Parsing Unexpected End

Can someone please explain me why I am getting an issue with this one line because for some reason when I run it with node in the console I'm receiving the Unexpected end of input at Object.parse(native) response.
var profile = JSON.parse(body);
Full code:
//Problem: We need a simple way to look at a user's badge count and Javascript points
//Solution: Use Node.js to connect to Treehouse's API to get profile information to print out
var http = require("http");
var username = "testuser";
//Print out message
function printMessage(username, badgeCount, points) {
var message = username + " has " + badgeCount + " total badge(s) and " + points + " points in Javascript";
console.log(message);
}
//Print out error messages
function printError(error) {
console.error(error.message);
}
//Connect to API URL (http://teamtreehouse.com/username.json)
var request = http.get("http://teamtreehouse.com/" + username + ".json", function(response) {
var body = "";
//Read the data
response.on('data', function(chunk) {
body += chunk;
});
response.on('end', function() {
if(response.statusCode == 200){
try {
var profile = JSON.parse(body);
printMessage(username, profile.badges.length, profile.points.Javascript);
} catch(error) {
//Parse Error
printError(error);
}
} else {
//Status Code Error
printError({message: "There was an error getting the profile for " + username +". (" + http.SSTATUS_CODES[response.statusCode] + ")"});
}
});
//Parse the data
//Print the data
});
//Connection Error
request.on('error', printError);
When I try to browse to http://teamtreehouse.com/test.json, it redirects me to the corresponding HTTPS url. Use the nodejs https module and use the https version of the url : https://teamtreehouse.com/test.json.
Or use the popular request module that can handle redirects and https : https://github.com/request/request. It is easier to use as well.

Node.JS Express to HTML data transfer

I am creating a login application with node.js, I seem to have ran into a knowledge deficit in the area of transferring strings from the server to html.
I posted my current code at jsfiddle.
My application verifies the credentials to the mysql table then generates a basic token that contains the username password and the ip address of the user.
In the last block of code, where the client html posts to the server, I have two segments where you see send to basic user page and send to admin page.
I have attempted to research this subject, but i get nothing pertinent to the situation. can anyone guide me in the right direction on sending the user to the admin or user page while sending the token alongside of it?
As well, how can the express server send data to the client, for example
on the page, I want the database to hold pertinant information regarding the user, like address and phone number. How can this information be transmitted from the server to the client via html?
app.post('/', urlencodedParser, function (req, res) {
var date = new Date();
con.query("SELECT * from users WHERE username=" + con.escape(req.body.username) + " AND password=" + con.escape(req.body.password), function (err, rows, fields) {
if (err) {
console.log(err);
} else {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (rows == '' && rows == '') {
console.log('User Failed to login to the server with #'.red + con.escape(req.body.username) + ':' + con.escape(req.body.password));
res.sendFile(__dirname + '/admin/failure.html');
} else {
var isadmin = rows[0].admin;
var cryptomap = [req.body.username + ',' + req.body.password + ',' + ip];
var strcryptomap = cryptomap.toString(); // convert array to string
var token = encrypt(strcryptomap);
console.log(token + ' SENT'.red);
var backto = decrypt(token); //decr
var arr = backto.toString().split(","); // SPLITTING STRING TO SATISFY /VERIFY *************************************************
console.log(arr[0] + ' has valid token, encryption succsessful'.green);
con.query('UPDATE users SET crypto=' + con.escape(token) + 'WHERE username=' + con.escape(req.body.username), function (err, rows, fields) {
if (err) {
res.send(500);
} else {
console.log('Updated Crypto for ' + req.body.username);
if (isadmin == 0) {
// send to basic user page
res.send('USER');
} else {
//send to admin user page
res.sendto('http://google.com/?' + token);
}
}
});
}
}
});
});
To start, I'll answer the actual question you are asking.
The way I normally handle what you are trying to accomplish, is by using an ajax POST from the front end with the users credentials(https of course, never send credentials using http), have the server authenticate the user, create the token and respond to the ajax post with the token. From here, if the authentication was successful and the server responded with a token and whatever other information you wanted to get, you have a few options for storing it, I personally use cookies. After the token is stored, let the front end handle the redirect.
Having said all of that, I would definitely read up on authentication principles and the different ways your system can be attacked. I see a couple of red flags dealing with pretty basic authentication ideas/strategies.
Edit : Here is an example AJAX post to a login API endpoint that responds with a token and saves the username and token to cookies. Obviously your result data in the success function may be organized differently than mine but can be accessed in the same way. You can send whatever data you would like back in this result object and redirect accordingly
var loginData = {
username : $('#loginUsername').val(),
password : $('#loginPassword').val()
}
$.ajax({
type : "POST",
url : [your-endpoint-url],
data : loginData ,
success : function(result) {
setCookie('appUN', result.username);
setCookie('appTok', result.token);
location.href = '/dashboard';
},
error : function(result) {
location.href = '/login/Error';
}
});
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + 10800000);
var expires = "expires="+d.toUTCString();
var path = "path=/";
document.cookie = cname + "=" + cvalue + "; " + expires + ";" + path;
}
To actually send the data back to the client from the server, in your API endpoint, you would do all of your logic to check the users credentials and if their credentials were valid, you could create a token and return something like
res.json({
username: username,
token: token
});
and this JSON object will be available in the success function as shown above.
If the users credentials were invalid, you could return something like
res.status(400).json({Message : "The username or password is incorrect"});
and because of the 400 status, it will be caught by the error function of your AJAX request

Meteor.http.get request -> Error: Hostname/IP doesn't match certificate's altnames

I'm trying to use meteor.http module and I'm getting the following error on the server side.
"Error: Hostname/IP doesn't match certificate's altnames" since I'm new in Meteor and in Node.js and its javaScript debugging is hard (btw how can I debug server side scripts ? client side it's easy), I'm using MAC OS X 10.9 not sure if it's relevent...
Thanks
Ronen
client side code:
'click #buildButton' : function () {
console.log("Jenkins job request");
$('#buildButton').attr('disabled','true').val('loading...');
var userName = "Ronen";
Meteor.call('jenkinsServiceBuild', function(err, respJson) {
if(err) {
window.alert("Error: " + err.reason);
console.log("error occured on receiving data on server. ", err );
} else {
window.alert("Success: ");
console.log("respJson: ", respJson);
//window.alert(respJson.length + ' tweets received.');
Session.set("recentTweets",respJson);
}
$('#buildButton').removeAttr('disabled').val('build');
});
}
Server Side Code:
Meteor.methods({jenkinsServiceBuild: function(userName) {
var url = "https://www.ynet.co.il";
//synchronous GET
var result = Meteor.http.get(url, {timeout:30000});
if(result.statusCode==200) {
var respJson = JSON.parse(result.content);
console.log("response received.");
return respJson;
} else {
console.log("Response issue: ", result.statusCode);
var errorJson = JSON.parse(result.content);
throw new Meteor.Error(result.statusCode, errorJson.error);
}
}
});
The site 'https://www.ynet.co.il' has an incorrectly installed SSL certficate for that domain. It's using akamai's certificate.
If you know and trust the site and its for nothing too secure just remove the s in https
var url = "http://www.ynet.co.il";
Also I'm not sure the code will work, looking at the site it serves html content but this line:
var respJson = JSON.parse(result.content);
Suggests it serves JSON content. If it does server json content use this instead:
var respJson = result.data;

Categories

Resources