Skip to content

Instantly share code, notes, and snippets.

@Aymkdn
Last active May 5, 2021 13:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Aymkdn/c0357668c016ad4ba676c6d812508b0b to your computer and use it in GitHub Desktop.
Save Aymkdn/c0357668c016ad4ba676c6d812508b0b to your computer and use it in GitHub Desktop.
Node JS way to get the FedAuth cookie for a Sharepoint 2019 OnPrem with AzureAD authentication – See https://github.com/Aymkdn/SharepointPlus/wiki/Using-the-FedAuth-Cookie/
const extend=require('extend');
const { sso } = require('node-expose-sspi');
const fetch = require('node-fetch');
var debugMode = false;
var globalCredentials = {};
/**
* Returns an object of cookies
* @param {Array} cookies An array of "cookieName=cookieValue;whatever…"
* @return {Object|Null} a JSON object with {'cookie name and ':'cookie value'}
*/
function getCookies(cookies) {
let ret = {};
if (Array.isArray(cookies) && cookies.length > 0) {
cookies.forEach(cookie => {
let [ key, val ] = cookie.split(';')[0].split('=');
ret[key] = val;
});
} else return null;
return ret;
}
/**
* Convert an object of {'cookie name and ':'cookie value'} to a string that can be used with headers.cookie
* @param {Object} cookies
* @return {String}
*/
function getCookieString(cookies) {
let str = "";
for (let key in cookies) {
str += key+'='+cookies[key]+'; ';
}
return str;
}
function getFormParams($form, $) {
let formParams = new URLSearchParams();
// search for parameters to post
$form.find('input').each((index, element) => {
let $element = $(element);
switch ($element.attr('type')) {
case "hidden":
case "password":
case "text": {
let name = $element.attr("name");
let value = "";
switch(name) {
case "pf.username": {
value = globalCredentials.username;
break;
}
case "pf.pass": {
value = globalCredentials.password;
break;
}
case "pf.ok":{
value = "clicked";
break;
}
default: {
value = $element.attr("value");
}
}
formParams.append(name, value);
break;
}
}
});
return formParams.toString().replace(/\+/g, '%20');
}
/**
* Use node-fetch to send a request
* @param {Object} params
* @param {String} uri The URL to call
* @params {…} any other params used by node-fetch
* @return {Promise}
*/
async function request(params) {
try {
let uri = params.uri;
if (debugMode) console.log('# Fetching "'+uri.slice(0,180)+'"…');
delete params.uri;
let options = extend({follow:0, redirect:'manual'}, params);
let response = await fetch(uri, options);
let body = await response.text();
// prepare for the next request
let opt = {
headers:{
'User-Agent':'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3)'
}
};
// retrieve the cookies
let cookies = getCookies(response.headers.raw()['set-cookie']);
// and merge them with the cookies we had
if (cookies) {
let paramsCookies = (params.headers && params.headers.cookie ? params.headers.cookie : null);
if (paramsCookies) {
paramsCookies = getCookies(paramsCookies.split('; '));
}
cookies = extend({}, paramsCookies || {}, cookies);
extend(opt.headers, {cookie:getCookieString(cookies)});
}
// Take different actions depending of the page URI and Status
switch (response.status) {
case 200: {
if (debugMode) console.log('# Status Code is 200');
if (uri.includes('/wsfed')) {
if (debugMode) console.log("# GetCredentialType…");
let flowToken, originalRequest, canary, clientRequestId, hpgrequestid, hpgact, hpgid;
let mtch = body.match(/"sCtx":"([^"]+)"/);
if (mtch) {
originalRequest = mtch[1];
}
mtch = body.match(/"sFT":"([^"]+)"/);
if (mtch) {
flowToken = mtch[1];
}
mtch = body.match(/"apiCanary":"([^"]+)"/);
if (mtch) {
canary = mtch[1];
}
mtch = body.match(/"correlationId":"([^"]+)"/);
if (mtch) {
clientRequestId = mtch[1];
}
mtch = body.match(/"sessionId":"([^"]+)"/);
if (mtch) {
hpgrequestid = mtch[1];
}
mtch = body.match(/"hpgact":(\d+)/);
if (mtch) {
hpgact = mtch[1];
}
mtch = body.match(/"hpgid":(\d+)/);
if (mtch) {
hpgid = mtch[1];
}
if (!originalRequest || !flowToken || !canary || !clientRequestId || !hpgrequestid || !hpgact || !hpgid) {
throw "Cannot find the auth parameters for 'GetCredentialType'…";
}
extend(opt, {
uri: "https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US",
method: "post",
body: JSON.stringify({"username":globalCredentials.email,"isOtherIdpSupported":true,"checkPhones":false,"isRemoteNGCSupported":true,"isCookieBannerShown":true,"isFidoSupported":true,"originalRequest":originalRequest,"country":"FR","forceotclogin":false,"isExternalFederationDisallowed":false,"isRemoteConnectSupported":false,"federationFlags":0,"isSignup":false,"flowToken":flowToken,"isAccessPassSupported":true}),
headers:{
'canary':canary,
'client-request-id': clientRequestId,
'hpgrequestid': hpgrequestid,
'hpgact': hpgact,
'hpgid': hpgid,
'Origin': "https://login.microsoftonline.com",
'Content-Type': 'application/json'
}
})
return request(opt);
} else if (uri.startsWith("https://login.microsoftonline.com/common/GetCredentialType")) {
let json = JSON.parse(body);
opt.uri = json.Credentials.FederationRedirectUrl;
return request(opt);
} else if (uri.includes('prp.ping') || uri.includes('login.srf')) {
// we should have a <form>
let cheerio = require('cheerio');
let $ = cheerio.load(body);
let $form = $('form');
if ($form.length > 0) {
if (debugMode) console.log("# The form has been found.");
extend(opt, {
uri: $form.attr('action'),
method: "post",
});
if (!opt.uri.startsWith('http')) {
opt.uri = uri.split("/").slice(0,3).join('/') + opt.uri;
}
opt.headers['Content-Type'] = 'application/x-www-form-urlencoded';
opt.body = getFormParams($form, $);
return request(opt);
}
}
throw "Unpredicted case…";
}
case 302: {
if (debugMode) console.log('# Status Code is 302');
// search if we have the FedAuth cookie
let cookies = (response.headers.raw()['set-cookie'] || []);
for (let i=0; i<cookies.length; i++) {
if (cookies[i].startsWith('FedAuth')) {
// WE GOT IT !!!!
if (debugMode) console.log("# FedAuth Found!");
return cookies[i];
}
}
// find the new location
opt.uri = response.headers.get('location');
return request(opt);
}
case 401: {
if (debugMode) console.log('# Status Code is 401');
let wwwAuth = response.headers.get('www-authenticate');
if (wwwAuth === 'Negotiate') {
if (debugMode) console.log("# Kerberos Authentication Required…");
extend(opt, {
follow:0,
redirect:'manual'
});
// we use 'node-expose-sspi'
if (debugMode) console.log('# Using sso.client.fetch with "'+uri+'"…');
let res = await new sso.Client().fetch(uri, opt);
if (res.status == 200) {
let body = await res.text();
// we should have have a <form>
let cheerio = require('cheerio');
let $ = cheerio.load(body);
let $form = $('form');
if ($form.length > 0) {
if (debugMode) console.log("# The form has been found.");
extend(opt, {
uri: $form.attr('action'),
method: "post"
});
// check the URI stats with 'http'
if (!opt.uri.startsWith('http')) {
// in that case we use the same domain that the last request
opt.uri = uri.split("/").slice(0,3).join('/') + opt.uri;
}
opt.headers['Content-Type'] = 'application/x-www-form-urlencoded';
opt.body = getFormParams($form, $);
// retrieve the cookie
// and merge them with the cookies we had
let newCookies = getCookies(res.headers.raw()['set-cookie']);
extend(opt.headers.cookie, getCookieString(newCookies));
return request(opt);
}
}
throw "Unknown step…";
}
}
}
} catch(err) {
console.log("# Error: ",err);
}
}
/**
* Retrieve the FedAuth cookie
* @param {Object} params
* @param {Object} credentials The credentials {username, password}
* @param {String} uri The start url
* @param {Boolean} [debug=false] To print debug info
* @return {Promise} the cookie value (FedAuth=…)
*/
async function getFedAuth(params) {
debugMode = (params.debug === true ? true : false);
if (debugMode) console.log("# Retrieve FedAuth...");
globalCredentials = params.credentials;
// analyze the URL to get the Sharepoint root
let rootSite = params.uri.split('/');
rootSite = rootSite.slice(0, (rootSite[3] === 'sites' ? 5 : 3)).join('/');
let response = await request({
uri:rootSite + '/_trust/default.aspx?trust=AzureAD&ReturnUrl=%2f_layouts%2f15%2fAuthenticate.aspx%3fSource%3d%2f'+encodeURIComponent(encodeURIComponent(params.uri))
});
//if (debugMode) console.log("# Cookie Retrieved: ", response);
return response;
}
exports["default"] = getFedAuth;
module.exports = exports.default;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment