Last active
May 5, 2021 13:36
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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