Skip to content
forked from simov/grant

OAuth Middleware for Express, Koa and Hapi

License

Notifications You must be signed in to change notification settings

JavaScriptExpert/grant

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Grant

npm-version travis-ci coveralls-status

180+ Supported Providers / OAuth Playground

23andme | 500px | acton | acuityscheduling | aha | amazon | angellist | arcgis | asana | assembla | auth0 | authentiq | axosoft | baidu | basecamp | battlenet | beatport | bitbucket | bitly | box | buffer | campaignmonitor | cheddar | clio | coinbase | concur | constantcontact | coursera | dailymile | dailymotion | deezer | delivery | deputy | deviantart | digitalocean | discogs | discord | disqus | docusign | dribbble | dropbox | ebay | echosign | ecwid | edmodo | egnyte | etsy | eventbrite | evernote | everyplay | eyeem | facebook | familysearch | feedly | fitbit | flattr | flickr | flowdock | fluidsurveys | formstack | foursquare | freeagent | freelancer | freshbooks | geeklist | genius | getbase | getpocket | gitbook | github | gitlab | gitter | goodreads | google | groove | gumroad | harvest | hellosign | heroku | homeaway | hootsuite | iconfinder | idme | idonethis | imgur | infusionsoft | instagram | intuit | jamendo | jumplead | kakao | letsfreckle | linkedin | live | lyft | mailchimp | mailup | mapmyfitness | mastodon | medium | meetup | microsoft | mixcloud | mixer | moves | moxtra | mydigipass | myob | nest | nylas | okta | onelogin | openstreetmap | optimizely | patreon | paypal | pinterest | plurk | podio | producteev | producthunt | projectplace | pushbullet | ravelry | redbooth | reddit | runkeeper | salesforce | shoeboxed | shopify | skyrock | slack | slice | smartsheet | smugmug | socialpilot | socrata | soundcloud | spotify | square | stackexchange | stocktwits | stormz | strava | stripe | surveygizmo | surveymonkey | thingiverse | ticketbud | timelyapp | todoist | trakt | traxo | trello | tripit | tumblr | twitch | twitter | typeform | uber | underarmour | unsplash | upwork | uservoice | vend | venmo | verticalresponse | viadeo | vimeo | visualstudio | vk | weekdone | weibo | withings | wordpress | wrike | xero | xing | yahoo | yammer | yandex | zendesk

Table of Contents

Express

npm install grant-express
var express = require('express')
var session = require('express-session')
var grant = require('grant-express')

var app = express()
// REQUIRED: any session store - see /examples/express-session-stores
app.use(session({secret: 'grant'}))
// mount grant
app.use(grant({/*configuration - see below*/}))

Koa

npm install grant-koa
var Koa = require('koa')
var session = require('koa-session')
var mount = require('koa-mount')
var grant = require('grant-koa')

var app = new Koa()
// REQUIRED: any session store - see /examples/koa-session-stores
app.keys = ['grant']
app.use(session(app))
// mount grant
app.use(mount(grant({/*configuration - see below*/})))

Hapi

npm install grant-hapi
var Hapi = require('hapi')
var yar = require('yar')
var grant = require('grant-hapi')

var server = new Hapi.Server()
server.register([
  // REQUIRED: any session store - see /examples/hapi-session-stores
  {plugin: yar, options: {cookieOptions: {password: 'grant', isSecure: false}}},
  // mount grant
  {plugin: grant(), options: {/*configuration - see below*/}}
])

Configuration

{
  "defaults": {
    "protocol": "http",
    "host": "localhost:3000",
    "callback": "/callback",
    "transport": "session",
    "state": true
  },
  "google": {
    "key": "...",
    "secret": "...",
    "scope": ["scope1", "scope2"],
    "nonce": true,
    "custom_params": {"access_type": "offline"},
    "callback": "/google/callback"
  },
  "twitter": {
    "key": "...",
    "secret": "..."
  }
}
  • defaults - default configuration for all providers (previously this option was called server)
    • protocol - either http or https
    • host - your server's host name localhost:3000 | dummy.com:5000 | mysite.com ...
    • path - path prefix to use for the Grant middleware (defaults to empty string if omitted)
    • callback - common callback route for all providers in your config /callback | /done ...
    • transport - transport to use to deliver the response data in your final callback route querystring | session (defaults to querystring if omitted)
    • state - generate random state string on each authorization attempt true | false (OAuth2 only, defaults to false if omitted)
  • provider - any supported provider facebook | twitter ...
    • key - consumer_key or client_id of your OAuth app
    • secret - consumer_secret or client_secret of your OAuth app
    • scope - array of OAuth scopes to request
    • callback - specific callback route to use for this provider only /callback | /done ...
    • custom_params - custom authorization parameters
    • nonce - generate random nonce string on each authorization attempt true | false (OpenID Connect only, defaults to false if omitted)
    • dynamic - list of options that can be overridden dynamically ['subdomain', 'scope']

Each provider can override any of the default options. The defaults key is completely optional.

Additionally every provider can override any of the reserved keys, some of which are predefined internally.

Redirect URL

For redirect URL of your OAuth application you should always use this format:

[protocol]://[host]/connect/[provider]/callback

Where protocol and host should match the ones from which you initiate the OAuth flow, and provider is the provider's name from the list of supported providers.

This redirect URL is used internally by Grant. You will receive the response data from the OAuth flow inside the route specified in the callback key of your Grant configuration.

See the Path Prefix section on how to configure the redirect URL when using the path configuration option.

Reserved Routes

Grant operates on the following routes:

/connect/:provider/:override?
/connect/:provider/callback

Path Prefix

You can mount Grant under specific path prefix:

// Express
app.use('/path/prefix', grant(config))
// Koa
app.use(mount('/path/prefix', grant(config)))
// Hapi
server.register([{routes: {prefix: '/path/prefix'}, plugin: grant(config)}])

In this case it is required to specify the path prefix using the path configuration option for the defaults key:

{
  "defaults": {
    "protocol": "...",
    "host": "...",
    "path": "/path/prefix"
  }
}

Lastly that path prefix should be specified in your OAuth application's redirect URL as well:

[protocol]://[host][path]/connect/[provider]/callback

In case you want your callback routes prefixed, set them accordingly:

{
  "facebook": {
    "callback": "/path/prefix/handle_facebook_callback"
  }
}

OpenID Connect

The nonce option is recommended when requesting the openid scope:

{
  "google": {
    "scope": ["openid"],
    "nonce": true
  }
}

Grant does not verify the signature of the returned id_token because that requires discovery, caching, and expiration of the provider's public keys.

However, Grant tries to decode the id_token and verifies the following two claims (returns error respectively):

  1. aud - is the token intended for my OAuth app?
  2. nonce - does it tie to a request of my own?

For convenience the response data contains the decoded id_token.

Take a look at the OpenID Connect example.

Custom Parameters

Some providers may employ custom authorization parameters, that you can pass using the custom_params option:

"google": {
  "custom_params": {"access_type": "offline"}
},
"reddit": {
  "custom_params": {"duration": "permanent"}
},
"trello": {
  "custom_params": {"name": "my app", "expiration": "never"}
}

Alternatively any custom parameter that is not a reserved key, and is listed under the custom_parameters array for that provider, can be defined along with the rest of the options.

Static Overrides

You can add arbitrary {object} keys inside your provider's configuration to create sub configurations that override the global settings for that provider:

// navigate to /connect/facebook
"facebook": {
  "key": "...",
  "secret": "...",
  // by default request publish permissions
  "scope": ["publish_actions", "publish_stream"],
  // set specific callback route on your server for this provider
  "callback": "/facebook/callback",
  // navigate to /connect/facebook/groups
  "groups": {
    // request only group permissions
    "scope": ["user_groups", "friends_groups"]
  },
  // navigate to /connect/facebook/pages
  "pages": {
    // request only page permissions
    "scope": ["manage_pages"],
    // additionally use specific callback route on your server for this override
    "callback": "/facebook_pages/callback"
  }
}

The custom key names cannot be one of the reserved keys.

Dynamic Override

In some cases you may want to allow the user to override certain parts of your configuration dynamically.

For example for shopify you have to embed the user's shop name into the OAuth URLs, so it makes sense to allow the subdomain option to be overridden dynamically:

{
  "shopify": {
    "dynamic": ["subdomain"]
  }
}

Then you can have a form on your website to allow the user to specify the shop name:

<form action="/connect/shopify" method="post" accept-charset="utf-8">
  <input type="text" name="subdomain" value="" />
  <button>submit</button>
</form>

Keep in mind that when making POST request to the /connect/:provider/:override? route you have to mount the body-parser middleware for Express and Koa before mounting Grant:

// express
var parser = require('body-parser')
app.use(parser.urlencoded({extended: true}))
app.use(grant(config))
// koa
var parser = require('koa-bodyparser')
app.use(parser())
app.use(mount(grant(config)))

Alternatively you can make a GET request to the /connect/:provider/:override? route:

https://mywebsite.com/connect/shopify?subdomain=usershop

Reserved Keys

List of all reserved keys and their typical location:

Key Location Description
request_url oauth.json OAuth1/step1
authorize_url oauth.json OAuth1/step2 or OAuth2/step1
access_url oauth.json OAuth1/step3 or OAuth2/step2
oauth oauth.json OAuth version number
scope_delimiter oauth.json string delimiter used for concatenating multiple scopes
custom_parameters oauth.json list of known custom authorization parameters
protocol, host, path defaults used to generate redirect_uri
transport defaults transport to use to deliver the response data in your final callback route
state defaults toggle random state string generation for OAuth2
key [provider] OAuth app key, reserved aliases: consumer_key and client_id
secret [provider] OAuth app secret, reserved aliases: consumer_secret and client_secret
scope [provider] list of scopes to request
custom_params [provider] custom authorization parameters and their values
subdomain [provider] string to be embedded in request_url, authorize_url and access_url
nonce [provider] toggle random nonce string generation for OpenID Connect providers
callback [provider] final callback route on your server to receive the response data
dynamic [provider] allow dynamic override of configuration
name generated provider's name, used to generate redirect_uri
[provider] generated provider's name as key
redirect_uri generated OAuth app redirect URI, generated using protocol, host, path and name
overrides generated all keys containing {object} value are extracted here as static overrides

Custom Providers

In case you have a private OAuth provider that you don't want to be part of the officially supported ones, you can define it in your configuration by adding a custom key for it.

In this case you have to specify all of the required provider keys by yourself:

{
  "defaults": {
    "protocol": "https",
    "host": "mywebsite.com"
  },
  "mywebsite": {
    "authorize_url": "https://mywebsite.com/authorize",
    "access_url": "https://mywebsite.com/token",
    "oauth": 2,
    "key": "[CLIENT_ID]",
    "secret": "[CLIENT_SECRET]",
    "scope": ["read", "write"]
  }
}

Refer to the Grant's OAuth configuration to see how various providers are configured.

Development Environments

You can easily configure different development environments:

{
  "development": {
    "defaults": {"protocol": "http", "host": "dummy.com:3000"},
    "facebook": {
      "key": "development OAuth app credentials",
      "secret": "development OAuth app credentials"
    }
  },
  "staging": {
    "defaults": {"protocol": "https", "host": "staging.mywebsite.com"},
    "facebook": {
      "key": "staging OAuth app credentials",
      "secret": "staging OAuth app credentials"
    }
  },
  "production": {
    "defaults": {"protocol": "https", "host": "mywebsite.com"},
    "facebook": {
      "key": "production OAuth app credentials",
      "secret": "production OAuth app credentials"
    }
  }
}

Then you can pass the environment flag:

NODE_ENV=production node app.js

And use it in your application:

var config = require('./config.json')
var grant = Grant(config[process.env.NODE_ENV || 'development'])

Subdomain

Some providers have dynamic URLs containing bits of user information embedded in them.

The subdomain option can be used to specify your company name, server region or whatever else is required:

"shopify": {
  "subdomain": "mycompany"
},
"battlenet": {
  "subdomain": "us"
}

Then Grant will generate the correct OAuth URLs:

"shopify": {
  "authorize_url": "https://mycompany.myshopify.com/admin/oauth/authorize",
  "access_url": "https://mycompany.myshopify.com/admin/oauth/access_token"
},
"battlenet": {
  "authorize_url": "https://us.battle.net/oauth/authorize",
  "access_url": "https://us.battle.net/oauth/token"
}

Alternatively you can override the entire authorize_url and access_url in your configuration.

Sandbox OAuth URLs

Some providers may have sandbox URLs for testing. To use them just override the entire request_url, authorize_url and access_url in your configuration (notice the sandbox bits):

"paypal": {
  "authorize_url": "https://www.sandbox.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
  "access_url": "https://api.sandbox.paypal.com/v1/identity/openidconnect/tokenservice"
},
"evernote": {
  "request_url": "https://sandbox.evernote.com/oauth",
  "authorize_url": "https://sandbox.evernote.com/OAuth.action",
  "access_url": "https://sandbox.evernote.com/oauth"
}

Sandbox Redirect URI

Very rarely you may need to override the default redirect_uri that Grant generates for you.

For example Feedly supports only http://localhost as redirect URL of their Sandbox OAuth application, and it won't allow the http://localhost/connect/feedly/callback path:

"feedly": {
  "redirect_uri": "http://localhost"
}

In this case you'll have to redirect the user to the [protocol]://[host]/connect/[provider]/callback route that Grant uses to execute the last step of the OAuth flow:

var qs = require('querystring')

app.get('/', (req, res) => {
  if (process.env.NODE_ENV === 'development' &&
      req.session.grant &&
      req.session.grant.provider === 'feedly' &&
      req.query.code
  ) {
    res.redirect(`/connect/${req.session.grant.provider}/callback?${qs.stringify(req.query)}`)
  }
})

As usual you will receive the response data in your final callback route.

More Dynamic Overrides

In case you really want to, you can allow dynamic overriding of every option for a provider:

{
  "facebook": {
    "dynamic": true
  }
}

And the most extreme case is allowing non preconfigured providers to be used dynamically:

{
  "defaults": {
    "dynamic": true
  }
}

Essentially Grant is an OAuth proxy.

Quirks

Ebay

Set the redirect URL of your OAuth app as usual [protocol]://[host]/connect/ebay/callback. Then Ebay will generate a special string called RuName (eBay Redirect URL name) that you need to set as redirect_uri in Grant:

"ebay": {
  "redirect_uri": "[RUNAME]"
}

Flickr, Freelancer, Optimizely

Some providers are using custom authorization parameter to pass the requested scopes - Flickr perms, Freelancer advanced_scopes, Optimizely scopes, but you can use the regular scope option in your configuration:

"flickr": {
  "scope": ["write"]
},
"freelancer": {
  "scope": ["1", "2"]
},
"optimizely": {
  "scope": ["all"]
}

Mastodon

Mastodon requires the entire domain of your server to be embedded in the OAuth URLs. However you should use the subdomain option:

"mastodon": {
  "subdomain": "mastodon.cloud"
}

SurveyMonkey

Set your Mashery user name as key and your application key as api_key:

"surveymonkey": {
  "key": "[MASHERY_USER_NAME]",
  "secret": "[CLIENT_SECRET]",
  "api_key": "[CLIENT_ID]"
}

Fitbit, LinkedIn, ProjectPlace

Initially these were OAuth1 providers, so the fitbit, linkedin and projectplace names are used for that. To use their OAuth2 flow append 2 at the end of their names:

"fitbit2": {
  // navigate to /connect/fitbit2
},
"linkedin2": {
  // navigate to /connect/linkedin2
},
"projectplace2": {
  // navigate to /connect/projectplace2
}

VisualStudio

Set your Client Secret as secret not the App Secret:

"visualstudio": {
  "key": "[APP_ID]",
  "secret": "[CLIENT_SECRET not APP_SECRET]"
}

Response Data

The OAuth response data is returned as querystring in your final callback - the one you specify using the callback key in your Grant configuration.

Alternatively the response data can be returned inside the session, see the configuration section above and the session transport example.

OAuth2

The access_token and the refresh_token (if present) are accessible directly. The id_token (OpenID Connect only) contains the decoded JWT, and raw contains the raw response data:

{
  id_token: {header: {...}, payload: {...}, signature: '...'},
  access_token: '...',
  refresh_token: '...',
  raw: {
    id_token: '...',
    access_token: '...',
    refresh_token: '...',
    some: 'other data'
  }
}

OAuth1

The access_token and the access_secret are accessible directly, raw contains the raw response data:

{
  access_token: '...',
  access_secret: '...',
  raw: {
    oauth_token: '...',
    oauth_token_secret: '...',
    some: 'other data'
  }
}

Error

In case of an error, the error key will be populated with the raw error data:

{
  error: {
    some: 'error data'
  }
}

Alternative Require

Alternatively you can require any of the middlewares directly from grant (each pair is identical):

// Express
var Grant = require('grant-express')
var Grant = require('grant').express()
// Koa
var Grant = require('grant-koa')
var Grant = require('grant').koa()
// Hapi
var Grant = require('grant-hapi')
var Grant = require('grant').hapi()

Alternative Instantiation

Grant can be instantiated with or without using the new keyword:

var Grant = require('grant-express|koa|hapi')
var grant = Grant(config)
// identical to:
var grant = new Grant(config)

Additionally Hapi can accept the configuration in two different ways:

server.register([{plugin: grant(config)}])
// identical to:
server.register([{plugin: grant(), options: config}])

Programmatic Access

Every Grant instance have a config property attached to it:

var grant = Grant(require('./config'))
console.log(grant.config)

It contains the generated configuration data that Grant uses internally.

You can use the config property to alter the Grant's behavior during runtime.

Keep in mind that this affects the entire Grant instance! Dynamic override might be more appropriate.

Session vs Querystring

Grant returns the result of the OAuth flow in the final callback route on your server:

{
  "facebook": {
    "callback": "/finally"
  }
}

By default Grant encodes the result as querystring: /finally?access_token=...

This potentially may leak private data in your server logs, especially if you are behind reverse proxy.

It is recommended to use the session transport instead:

{
  "defaults": {
    "transport": "session"
  },
  "facebook": {
    "callback": "/finally"
  }
}

This way the result will no longer be encoded in the querystring: /finally

And you will receive the response data inside the session instead.

Redirect URI

The protocol, the host (and optionally the path) options are used to generate the correct redirect_uri for each provider:

{
  "defaults": {
    "protocol": "https",
    "host": "website.com"
  },
  "facebook": {},
  "twitter": {}
}

The above configuration is identical to:

{
  "facebook": {
    "redirect_uri": "https://website.com/connect/facebook/callback"
  },
  "twitter": {
    "redirect_uri": "https://website.com/connect/twitter/callback"
  }
}

Note that redirect_uri would override protocol and host even if they were specified.

Meta Configuration

You can document your configuration by adding custom keys to it:

{
  "google": {
    "app": "My Awesome OAuth App",
    "owner": "my_email@gmail.com",
    "url": "https://url/to/manage/oauth/app"
  }
}

These custom keys cannot be reserved ones, and cannot contain {object} value.

Get User Profile

Once you have your access tokens secured, you can start making authorized requests on behalf of your users.

For example, you may want to get the user's profile after the OAuth flow has completed:

var express = require('express')
var session = require('express-session')
var grant = require('grant-express')
var request = require('request-compose').client

var config = {
  "defaults": {
    "protocol": "http",
    "host": "localhost:3000"
  },
  "facebook": {
    "key": "[APP_ID]",
    "secret": "[APP_SECRET]",
    "callback": "/facebook_callback"
  }
}

express()
  .use(session({secret: 'grant', saveUninitialized: true, resave: true}))
  .use(grant(config))
  .get('/facebook_callback', async (req, res) => {
    var {body} = await request({
      url: 'https://graph.facebook.com/me',
      headers: {authorization: `Bearer ${req.query.access_token}`}
    })
    res.end(JSON.stringify({oauth: req.query, profile: body}, null, 2))
  })
  .listen(3000)

About

OAuth Middleware for Express, Koa and Hapi

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 99.4%
  • Shell 0.6%