Skip to content

Instantly share code, notes, and snippets.

@Aymkdn
Last active May 5, 2021 13:36
Show Gist options
  • Select an option

  • Save Aymkdn/c0357668c016ad4ba676c6d812508b0b to your computer and use it in GitHub Desktop.

Select an option

Save Aymkdn/c0357668c016ad4ba676c6d812508b0b to your computer and use it in GitHub Desktop.

Revisions

  1. Aymkdn revised this gist May 5, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion fedauth.js
    Original file line number Diff line number Diff line change
    @@ -208,7 +208,7 @@ async function request(params) {
    return request(opt);
    }
    case 401: {
    if (debugMode) console.log('# Status Code is 200');
    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…");
  2. Aymkdn created this gist Apr 10, 2020.
    286 changes: 286 additions & 0 deletions fedauth.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,286 @@
    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 200');
    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;