Skip to content

Latest commit

 

History

History
983 lines (729 loc) · 36 KB

README.md

File metadata and controls

983 lines (729 loc) · 36 KB

Grant

npm-version travis-ci coveralls-status

180+ Supported Providers / OAuth Playground

23andme | 500px | acton | acuityscheduling | aha | amazon | angellist | arcgis | asana | assembla | atlassian | auth0 | authentiq | aweber | axosoft | baidu | basecamp | battlenet | beatport | bitbucket | bitly | box | buffer | campaignmonitor | cheddar | clio | coinbase | concur | constantcontact | coursera | dailymotion | deezer | delivery | deputy | deviantart | digitalocean | discogs | discord | disqus | docusign | dribbble | dropbox | ebay | echosign | ecwid | edmodo | egnyte | etsy | eventbrite | evernote | eyeem | facebook | familysearch | feedly | fitbit | flattr | flickr | flowdock | formstack | foursquare | freeagent | freelancer | freshbooks | geeklist | genius | getbase | getpocket | gitbook | github | gitlab | gitter | goodreads | google | groove | gumroad | harvest | hellosign | heroku | homeaway | hootsuite | ibm | iconfinder | idme | idonethis | imgur | infusionsoft | instagram | intuit | jamendo | jumplead | kakao | line | linkedin | live | lyft | mailchimp | mailup | mailxpert | mapmyfitness | mastodon | medium | meetup | mention | microsoft | mixcloud | mixer | moxtra | myob | naver | nest | nokotime | nylas | okta | onelogin | openstreetmap | optimizely | patreon | paypal | phantauth | pinterest | plurk | podio | producthunt | projectplace | pushbullet | qq | ravelry | redbooth | reddit | runkeeper | salesforce | shoeboxed | shopify | skyrock | slack | slice | smartsheet | smugmug | snapchat | 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 | wechat | weekdone | weibo | withings | wordpress | wrike | xero | xing | yahoo | yammer | yandex | zendesk | zoom

Table of Contents


Middlewares

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 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(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*/}}
])

Basics

Configuration

{
  "defaults": {
    "protocol": "http",
    "host": "localhost:3000",
    "transport": "session",
    "state": true
  },
  "google": {
    "key": "...",
    "secret": "...",
    "scope": ["openid"],
    "nonce": true,
    "custom_params": {"access_type": "offline"},
    "callback": "/hello"
  },
  "twitter": {
    "key": "...",
    "secret": "...",
    "callback": "/hi"
  }
}
  • 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 | awesome.com ...
    • 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 google | 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
    • nonce - generate random nonce string on each authorization attempt true | false (OpenID Connect only, defaults to false if omitted)
    • custom_params - custom authorization parameters
    • callback - specific callback route to use for this provider only /callback | /done ...

List of all available options.

Reserved Routes

Grant operates on the following two routes:

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

You login by navigating to the /connect/:provider route where :provider is a key in your configuration, usually one of the officially supported providers. Additionally you can login through a static override key defined for that provider, in your configuration, by navigating to the /connect/:provider/:override? route.

Redirect URL

You should always use the following format for the redirect_uri of your OAuth App:

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

The protocol and the host are the options you specify inside the defaults key of your configuration, and this is where your server accepts incoming requests. The provider is a key in your configuration, usually one of the officially supported providers.

Note that the /connect/:provider/callback route is used internally by Grant! You will receive the OAuth response data inside the callback route specified in your configuration.

Path Prefix

You can mount Grant under specific path prefix:

// Express
app.use('/path/prefix', grant(config))
// Koa - using koa-mount
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 that path prefix in your configuration:

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

That path prefix should also be specified in your OAuth App Redirect URL:

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

Optionally you can prefix your callback routes as well:

{
  "github": {
    "callback": "/path/prefix/hello"
  }
}

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 configure using the custom_params option:

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

Static Overrides

You can specify provider sub configurations using the overrides key:

{
  "github": {
    "key": "...", "secret": "...",
    "scope": ["public_repo"],
    "callback": "/hello",
    "overrides": {
      "notifications": {
        "key": "...", "secret": "...",
        "scope": ["notifications"]
      },
      "all": {
        "scope": ["repo", "gist", "user"],
        "callback": "/awesome"
      }
    }
  }
}

Navigate to:

  • /connect/github to request the public_repo scope
  • /connect/github/notifications to request the notifications scope using another OAuth App (key and secret)
  • /connect/github/all to request a bunch of scopes and also receive the response data in another callback route

Dynamic Override

In some cases you may want certain parts of your configuration to be set 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 set dynamically:

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

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

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

Keep in mind that when making a 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(grant(config))

Alternatively you can use a GET request for the /connect/:provider/:override? route:

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

Response Data

The OAuth response data is returned in your final callback route.

  • By default the OAuth response data will be encoded as querystring.

  • You can instruct Grant to return the response data inside the session instead instead, by using the transport option.

  • You can limit the amount of returned data by using the response option.

OAuth 2.0

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

The refresh_token is optional. The id_token is returned only for OpenID Connect providers requesting the openid scope.

OAuth 1.0a

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

Error

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

In case of an error, the error key will contain the error data.

Response Data Transport

By default Grant will encode the OAuth response data as querystring in your final callback route:

{
  "github": {
    "callback": "/hello"
  }
}

This final /hello?access_token=... redirect 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"
  },
  "github": {
    "callback": "/hello"
  }
}

That way the result will no longer be encoded as querystring, and you will receive the response data inside the session instead.

Limit Response Data

By default Grant will return all available response data in your final callback route:

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

However, encoding potentially large amounts of data as querystring can lead to incompatibility issues with some servers and browsers, and generally is considered a bad practice.

It can also cause problems when the session transport is used, and the particular session store implementation encodes the entire session in a cookie, not just the session ID. In this case some servers may reject the HTTP request because of too big HTTP headers in size.

The response option can be used to limit the response data:

{
  "defaults": {
    "response": "tokens"
  }
}

This will return only the tokens, without the raw key.

In case you want to include the decoded id_token as well:

{
  "google": {
    "response": ["tokens", "jwt"]
  }
}

This will make the decoded id_token available as id_token_jwt in the response object.

Session

Grant uses session to persist state between HTTP redirects occurring during the OAuth flow. This session, however, was never meant to be used as persistent storage, even if that's totally possible.

Once you receive the response data in your final callback route you are free to destroy that session, and do whatever you want with the returned data.

However, there are a few session keys returned in your final callback route, that you may find useful:

Key Availability Description
provider Always The provider name this authorization was called for
override Depends on URL The static override name used for this authorization
dynamic Depends on request type The dynamic override configuration passed for this authorization
state OAuth 2.0 only OAuth 2.0 state string that was generated
nonce OpenID Connect only OpenID Connect nonce string that was generated
response Depends on transport used The final response data
request OAuth 1.0a only Data returned from the first request of the OAuth 1.0a flow

Advanced

All Available Options

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
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
overrides [provider] static overrides for a provider
response [provider] limit the response data
token_endpoint_auth_method [provider] authentication method for the token endpoint
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

Configuration Scopes

Grant relies on configuration gathered from 5 different places:

  1. The first place Grant looks for configuration is the built-in oauth.json file located in the config folder.

  2. The second place Grant looks for configuration is the defaults key, specified in the user's configuration. These defaults are applied for every provider in the user's configuration.

  3. The third place for configuration is the provider itself. All providers in the user's configuration inherit every option defined for them in the oauth.json file, and all options defined inside the defaults key. Having oauth.json file and a defaults configuration is only a convenience. You can define all available options directly for a provider.

  4. The fourth place for configuration is the provider's overrides. The static overrides inherit their parent provider, essentially creating a sub provider of the same type.

  5. The fifth place for configuration, that potentially can override all of the above, and make all of the above optional, is the dynamic override.

Custom Providers

You can define your own provider by adding a key for it in your configuration. In this case you'll have to supply all of the required options by yourself:

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

Take a look at the oauth.json file to see how various providers are configured.

Development Environments

You can easily configure different development environments:

{
  "development": {
    "defaults": {"protocol": "http", "host": "localhost:3000"},
    "github": {
      "key": "development OAuth app credentials",
      "secret": "development OAuth app credentials"
    }
  },
  "staging": {
    "defaults": {"protocol": "https", "host": "staging.awesome.com"},
    "github": {
      "key": "staging OAuth app credentials",
      "secret": "staging OAuth app credentials"
    }
  },
  "production": {
    "defaults": {"protocol": "https", "host": "awesome.com"},
    "github": {
      "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'])

OAuth Proxy

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

{
  "github": {
    "dynamic": true
  }
}

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

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

Essentially Grant is a completely transparent OAuth Proxy.

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": "awesome.com"
  },
  "google": {},
  "twitter": {}
}

The above configuration is identical to:

{
  "google": {
    "redirect_uri": "https://awesome.com/connect/google/callback"
  },
  "twitter": {
    "redirect_uri": "https://awesome.com/connect/twitter/callback"
  }
}

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

Token Endpoint Auth Method

Grant is handling the OAuth 2.0 token endpoint request internally.

By default all of the required OAuth parameters will be encoded in the request body, including the OAuth app credentials.

You can use the token_endpoint_auth_method option to send the OAuth app credentials as Basic authorization header instead:

"google": {
  "token_endpoint_auth_method": "client_secret_basic"
}

Meta Configuration

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

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

In the example above meta is an arbitrary key, but it cannot be one of the reserved keys.


OAuth Quirks

Subdomain URLs

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 to use while developing your app. 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 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 correct http://localhost/connect/feedly/callback URL:

"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.

Provider 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 instead:

"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",
  "custom_params": {"api_key": "CLIENT_ID"}
}

Fitbit, LinkedIn, ProjectPlace

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

"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 instead of APP_SECRET"
}

Misc

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 accepts 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 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! Use dynamic override instead, to alter configuration per authorization attempt.

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": "/hello"
  }
}

express()
  .use(session({secret: 'grant', saveUninitialized: true, resave: true}))
  .use(grant(config))
  .get('/hello', 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)