Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose custom agent parameter? #93

Closed
bitinn opened this issue Oct 6, 2018 · 10 comments
Closed

Expose custom agent parameter? #93

bitinn opened this issue Oct 6, 2018 · 10 comments

Comments

@bitinn
Copy link
Contributor

bitinn commented Oct 6, 2018

Since purest allows for it, I would love to get the same support for grant. That would allow me to control the whole oauth + user profile fetching flow.

Namely, I want to use this with a proxy agent like: https://github.com/TooTallNate/node-proxy-agent

This is necessary as nodejs core still haven't expose an overridable http.globalAgent api yet, sorry for nagging you on this: nodejs/node#23281

@simov
Copy link
Owner

simov commented Oct 6, 2018

Thanks for the feedback, @bitinn! I'll think about it. I can see how this can be useful.

@simov
Copy link
Owner

simov commented Dec 7, 2018

👋 @bitinn I was thinking about this issue, and while I may never expose the underlying HTTP library, I think it will be possible to ... well, monkey-patch it:

var https = require('https')
var agent = new https.Agent({})

var request = require('grant/lib/client.js')

require.cache[require.resolve('grant/lib/client.js')].exports = (options) => {
  options.agent = agent
  return request(options)
}

Just make sure you execute this code after Grant has been required.

The options argument expects request-compose options that in turn can be any HTTP option from the underlying Node Core API, including agent.

Let me know what do you think!

@bitinn
Copy link
Contributor Author

bitinn commented Dec 7, 2018

Thx I will give it a go when available, a bit busy lately

@simov
Copy link
Owner

simov commented Jun 7, 2020

👋 Just released v5.2.0 with support for request options, take a look at the examples as well.

@simov simov closed this as completed Jun 7, 2020
@nathanbrizzee
Copy link

@bitinn I was thinking about this issue, and while I may never expose the underlying HTTP library, I think it will be possible to ... well, monkey-patch it:

var https = require('https')
var agent = new https.Agent({})

var request = require('grant/lib/client.js')

require.cache[require.resolve('grant/lib/client.js')].exports = (options) => {
  options.agent = agent
  return request(options)
}

Just make sure you execute this code after Grant has been required.

The options argument expects request-compose options that in turn can be any HTTP option from the underlying Node Core API, including agent.

Let me know what do you think!

Hi @simov , I am using @feathersjs/authentication-oauth (v 4.5.6) which uses this library. Unfortunately, it still uses version 4.7.0 and hasn't been updated to version 5. I've tried the Monkey Patch code and I can't get it to work no matter what I try. I feel that since 2018 when this was written, the library has changed enough that the patch shown isn't compatible. Is there a tweak I can do to this example to get it to work with version 4.7 (since it's embedded in FeathersJS)? I've spent several hours trying many different options. Thanks

@simov
Copy link
Owner

simov commented Jul 14, 2020

I'm pretty sure nothing changed in Grant regarding the way the internal HTTP client is being exposed. Can you provide a short code example about initializing FeathersJS?

@nathanbrizzee
Copy link

@simov, Here are the steps to create a full Feathersjs server with Microsoft Azure OAuth (on Ubuntu 20.04). This shows the code I tried to add to monkey patch the options variable that didn't work.

  • Install the feathers generator cli with: sudo npm install -g @feathersjs/cli
  • Create a project folder with whatever name you like: mkdir granttest && cd granttest
  • Generate a brand new feathersjs app. (Accept ALL the default prompts): feathers generate app
  • Edit default.json and add the following oauth section (the local section is shown for reference):
  "local": {
      "usernameField": "email",
      "passwordField": "password"
    },
    "oauth": {
      "redirect": "/",
      "microsoft": {
        "activeDirectoryGroups": [],
        "subdomain": "MICROSOFT_DIR_TENANT_ID",
        "key": "MICROSOFT_APP_CLIENT_ID",
        "secret": "MICROSOFT_CLIENT_SECRET",
        "authorize_url": "https://login.microsoftonline.com/[subdomain]/oauth2/v2.0/authorize",
        "access_url": "https://login.microsoftonline.com/[subdomain]/oauth2/v2.0/token",
        "profile_url": "https://graph.microsoft.com/v1.0/me",
        "memberof_url": "https://graph.microsoft.com/v1.0/me/memberOf?$select=displayName",
        "custom_params": {
          "response_type": "code",
          "response_mode": "query",
          "grant_type": "authorization_code"
        },
        "scope": [
          "https://graph.microsoft.com/offline_access",
          "https://graph.microsoft.com/openid",
          "https://graph.microsoft.com/profile",
          "https://graph.microsoft.com/email"
        ],
        "state": true,
        "nonce": true
      }
    }    
  • Edit the file authentication.js and make it look like the following (I added only two lines for azure. The rest was auto generated):
const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication');
const { LocalStrategy } = require('@feathersjs/authentication-local');
const { expressOauth } = require('@feathersjs/authentication-oauth');
const { AzureADStrategy } = require('./authentication-azure.js');

module.exports = app => {
  const authentication = new AuthenticationService(app);

  authentication.register('jwt', new JWTStrategy());
  authentication.register('local', new LocalStrategy());
  authentication.register('microsoft', new AzureADStrategy());

  app.use('/authentication', authentication);
  app.configure(expressOauth());
};
  • Create a new file called authentication-azure.js in the same folder as authentication.js. Add the following code to it. This is the code I am trying to patch. You will see my code changes near the top. Yes I know I'm not doing anything with the variable proxyAddr . I removed a bunch of code that didn't do anything but left this here.
const { OAuthStrategy } = require('@feathersjs/authentication-oauth');
const axios = require('axios');
const feathersErrors = require('@feathersjs/errors');
// https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/12345678-1234-1234-12345678901234567/isMSAApp/
// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols

// HTTP/HTTPS proxy to connect to
var proxyAddr = process.env.http_proxy || 'http://localhost:8009';
console.log('using proxy server ', proxyAddr);

// https://github.com/simov/grant/issues/93
var https = require('https');
const moduleName = 'grant/lib/client.js';
var request = require(moduleName);

var agent = new https.Agent({});
const fooa = require.cache[require.resolve(moduleName)]; //eslint-disable-line
console.log(fooa);

require.cache[require.resolve(moduleName)].exports = (options) => {
  options.agent = agent;
  return request(options);
};

const foob = require.cache[require.resolve(moduleName)]; //eslint-disable-line
console.dir(foob);

class AzureADStrategy extends OAuthStrategy {
  async getEntityData(profile) {
    const baseData = await super.getEntityData(profile);
    console.dir(baseData);
    console.dir(profile);

    return {
      ...baseData,
      email: profile.mail,
      firstName: profile.givenName,
      lastName: profile.surname,
      displayName: profile.displayName,
    };
  }

  // https://docs.microsoft.com/en-us/graph/api/user-list-memberof?view=graph-rest-1.0&tabs=http
  async getMemberOfGroups(data, params) {
    const url = this.configuration.memberof_url || '';
    if (!url) {
      throw new feathersErrors.GeneralError(
        'Missing memberof_url in configuration'
      );
    }
    if (params.authenticated === true && data.access_token) {
      try {
        // fetch data from a url endpoint
        const memberInfo = await axios.get(url, {
          headers: {
            'user-agent': 'feathers-azure-authentication',
            Accept: 'application/json',
            Authorization: `Bearer ${data.access_token}`,
          },
        });
        return memberInfo.data || {};
      } catch (error) {
        throw new feathersErrors.GeneralError(error);
      }
    } else {
      throw new feathersErrors.NotAuthenticated(
        'Not authorized - missing access_token'
      );
    }
  }

  async getProfile(data, params) {
    const profile = await super.getProfile(data, params);
    const memberOf = await this.getMemberOfGroups(data, params);
    const ADGroups = memberOf.value || [];

    // If there was a list of AD groups to check for
    let groupFound = true;
    if (
      // Get allowed list from default.json
      this.configuration.activeDirectoryGroups &&
      this.configuration.activeDirectoryGroups.length > 0
    ) {
      groupFound = this.configuration.activeDirectoryGroups.some(
        (groupToCheckFor) => {
          return ADGroups.some((adGroup) => {
            return adGroup.displayName === groupToCheckFor; // Look for an exact match (Case Sensitive).
          });
        }
      );
    }
    if (!groupFound) {
      throw new feathersErrors.NotAuthenticated(
        'User must be a member of one of the valid Azure Active Directory groups for this API set'
      );
    }

    const retval = {
      ...profile,
      memberOf: {
        ...memberOf,
      },
    };
    return retval;
  }
}

exports.AzureADStrategy = AzureADStrategy;

And, if you want to test with Microsoft env vars, a quick hack is to modify the run script in package.json . Note, you'll have to modify the values of the tenant_id, client_id, and client_secret from your Azure configuration for you app ID and set the redirect back to http://localhost:3030/

  "scripts": {
    "test": "npm run lint && npm run mocha",
    "lint": "eslint src/. test/. --config .eslintrc.json --fix",
    "dev": "MICROSOFT_DIR_TENANT_ID=11111111-2222-3333-4444-555555555555 MICROSOFT_APP_CLIENT_ID=11111111-2222-3333-4444-555555555555 MICROSOFT_CLIENT_SECRET=12345678901234567890123456789012 nodemon src/",
    "start": "node src/",
    "mocha": "mocha test/ --recursive --exit"
  },

Now run the server with npm run dev and visit http://localhost:3030/oauth/microsoft from your browser .

I tried all sorts of different ways to get ahold of the options variable so I could change the proxy but nothing I tried worked.
I use VSCode for my testing and used it to debug through the code. If you have any ideas on how to get this to work it would be greatly appreciated. My work proxies all internet access. I want to proxy my feathersjs app with CNTLM on my local box out to the internet because we use an authenticated proxy that has username/password info for Windows. Essentially proxy the proxy. The proxy wreaks havoc with applications that don't know how to be configured and use the proxy appropriately. The Grant library completely fails since it doesn't expose the proxy options. This all (Grant library plus FeathersJS) works fine on my laptop at home where there is no proxy.
Thank you in advance.

@simov
Copy link
Owner

simov commented Jul 20, 2020

Thanks for the detailed instructions @nathanbrizzee, I really appreciate the effort you put into this.

The monkey patch have to be applied after initializing Grant, because it have to be loaded in the NPM cache in order to patch it, but the patch itself have to be loaded before any other code that relies on Grant, and therefore may potentially initialize and cache some state on their own.

Just to be safe I decided to put the patch at the top of src/index.js before any other code:

// src/index.js
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0

var tunnel = require('tunnel')
var agent = tunnel.httpsOverHttp({
  proxy: {host: 'localhost', port: 8009}
})

require('grant')
var request = require('grant/lib/client.js')
require.cache[require.resolve('grant/lib/client.js')].exports = (options) => {
  options.agent = agent
  return request(options)
}

/* eslint-disable no-console */
const logger = require('./logger')
const app = require('./app')
const port = app.get('port')
const server = app.listen(port)

process.on('unhandledRejection', (reason, p) =>
  logger.error('Unhandled Rejection at: Promise ', p, reason)
)

server.on('listening', () =>
  logger.info('Feathers application started on http://%s:%d', app.get('host'), port)
)

I tested with a local mitm-proxy that I have and it works.

You can also have a look at the examples on alternative agents that you can use.

@nathanbrizzee
Copy link

nathanbrizzee commented Jul 21, 2020

Hi @simov , Thanks for the write-up! I copied your code into index.js as you showed. It worked for the first two login send.js calls (login.microsoftonline.com) but the third call (graph.microsoft.com) didn't receive the patch (in send.js) so it timed out on the call. I tried if a few times debugging along the way. Not sure why the first two calls get patched and the third call doesn't get patched.

I then added a new file called proxy.js in the same folder as index.js and moved the code there so I could import it elsewhere (for an axios call). Here's a copy of that code.

const url = require('url');
const tunnel = require('tunnel');

const proxy = {
  agent: null,
  proxyAddr: process.env.https_proxy || process.env.http_proxy || ''
};

if (proxy.proxyAddr) {
  // process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
  const q = url.parse(proxy.proxyAddr, true);
  const proxyInfo = {
    proxy: {
      host: q.hostname, 
      port: q.port,
      headers: {
        'User-Agent': 'FeathersJS'
      }
    }
  };
  
  if (q.auth) {
    proxyInfo.proxy.proxyAuth = q.auth;
  }
  proxy.agent = tunnel.httpsOverHttp(proxyInfo);

  require('grant');
  const request = require('grant/lib/client.js');
  require.cache[require.resolve('grant/lib/client.js')].exports = (options) => {
    options.agent = proxy.agent;
    return request(options);
  };
}

module.exports = proxy;

Then in index.js I added the require as the first line which should accomplish the same thing as your example code.

// src/index.js
/* eslint-disable no-console */
// Proxy must be the first line to monkey patch the Grant library
const proxy = require('./proxy'); // eslint-disable-line 
const logger = require('./logger');
const app = require('./app');
const port = app.get('port') || app.get('serverport');
const server = app.listen(port);

process.on('unhandledRejection', (reason, p) => {
...

In authentication-azure.js, I added some code for adding the proxy to axios, but I never reach that code because of the timeout on the graph api call earlier.

const { OAuthStrategy } = require('@feathersjs/authentication-oauth');
const ax = require('axios').default;
const feathersErrors = require('@feathersjs/errors');
const proxy = require('./proxy'); // eslint-disable-line 

const config = { };
if (proxy.agent !== null) {
  config.httpsAgent = proxy.agent;
  config.httpAgent = proxy.agent;
}
const axios = ax.create(config);
// https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/c0cf535a-bb4d-4731-94fb-8a4165b1a124/isMSAApp/

class AzureADStrategy extends OAuthStrategy {
  async getEntityData(profile) {
...

My other solution that is working is to clone the request-compose package into a local sub folder of my Feathersjs project and then modify send.js to add the proxy agent to the options variable and do a local npm install into my feathersjs project. That always works but now I have code that has been forked from the main repo.

@simov
Copy link
Owner

simov commented Jul 22, 2020

I see, I totally forgot about the grant-profile request. Unfortunately that's going to be impossible to monkey patch because grant-profile wraps the call to the request-compose extend method. I intentionally made it so that monkey patching is impossible once the extend method was called, because that can lead to really hard to debug issues, and generally unexpected behavior.

Patching request-compose send method and bundling it with your project is your only choice I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants