Comparing version 4.7.0 to 5.0.0
# Change Log | ||
## v5.0.0 (2020/04/18) | ||
- **Breaking:** Drop support for Node v4 and v6 | ||
- **Breaking:** Return `id_token` as string by default | ||
- **Breaking:** Change in the `response` configuration | ||
- **New:** `origin` and `prefix` configuration | ||
- **Deprecate:** Koa v1 and Hapi <= v16 | ||
- **Deprecate:** `protocol`, `host`, and `path` configuration | ||
- **[Migration Guide: from v4 to v5](https://github.com/simov/grant/blob/master/MIGRATION.md)** | ||
## v4.7.0 (2020/01/26) | ||
@@ -5,0 +14,0 @@ - **New:** [PKCE](https://github.com/simov/grant/commit/3b04eb69a278165ae9be7ba7a06e8b85da21c5e5) support |
@@ -27,4 +27,3 @@ { | ||
"access_url": "https://[subdomain].aha.io/oauth/token", | ||
"oauth": 2, | ||
"subdomain": true | ||
"oauth": 2 | ||
}, | ||
@@ -35,4 +34,3 @@ "amazon": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters" : ["scope_data"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -65,6 +63,3 @@ "angellist": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": [ | ||
"audience", "prompt" | ||
] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -75,5 +70,3 @@ "auth0": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true, | ||
"custom_parameters": ["audience", "prompt"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -84,6 +77,3 @@ "authentiq": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": [ | ||
"display", "response_mode", "prompt", "max_age", "ui_locales" | ||
] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -100,4 +90,3 @@ "aweber": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true | ||
"scope_delimiter": " " | ||
}, | ||
@@ -107,4 +96,3 @@ "baidu": { | ||
"access_url": "https://openapi.baidu.com/oauth/2.0/token", | ||
"oauth": 2, | ||
"custom_parameters": ["display", "force_login", "confirm_login", "login_type"] | ||
"oauth": 2 | ||
}, | ||
@@ -120,4 +108,3 @@ "basecamp": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true | ||
"scope_delimiter": " " | ||
}, | ||
@@ -171,4 +158,3 @@ "beatport": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["layout", "referral", "account", "meta"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -178,4 +164,3 @@ "concur": { | ||
"access_url": "https://[subdomain].api.concursolutions.com/oauth2/v0/token", | ||
"oauth": 2, | ||
"subdomain": true | ||
"oauth": 2 | ||
}, | ||
@@ -196,4 +181,3 @@ "constantcontact": { | ||
"access_url": "https://api.dailymotion.com/oauth/token", | ||
"oauth": 2, | ||
"custom_parameters": ["display"] | ||
"oauth": 2 | ||
}, | ||
@@ -258,7 +242,3 @@ "deezer": { | ||
"access_url": "https://api.dropboxapi.com/oauth2/token", | ||
"oauth": 2, | ||
"custom_parameters": [ | ||
"require_role", "force_reapprove", "disable_signup", "locale", | ||
"force_reauthentication" | ||
] | ||
"oauth": 2 | ||
}, | ||
@@ -269,4 +249,3 @@ "ebay": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["prompt"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -295,5 +274,3 @@ "echosign": { | ||
"oauth": 2, | ||
"subdomain": true, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["mobile"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -343,4 +320,3 @@ "etsy": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["prompt"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -357,4 +333,3 @@ "flattr": { | ||
"access_url": "https://www.flickr.com/services/oauth/access_token", | ||
"oauth": 1, | ||
"custom_parameters": ["perms"] | ||
"oauth": 1 | ||
}, | ||
@@ -386,4 +361,3 @@ "flowdock": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["advanced_scopes", "prompt"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -394,4 +368,3 @@ "freshbooks": { | ||
"access_url": "https://[subdomain].freshbooks.com/oauth/oauth_access.php", | ||
"oauth": 1, | ||
"subdomain": true | ||
"oauth": 1 | ||
}, | ||
@@ -430,4 +403,3 @@ "geeklist": { | ||
"access_url": "https://github.com/login/oauth/access_token", | ||
"oauth": 2, | ||
"custom_parameters": ["login", "allow_signup"] | ||
"oauth": 2 | ||
}, | ||
@@ -454,7 +426,3 @@ "gitlab": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": [ | ||
"access_type", "approval_prompt", "login_hint", "include_granted_scopes", | ||
"prompt", "display", "hd" | ||
] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -498,4 +466,4 @@ "groove": { | ||
"ibm": { | ||
"authorize_url": "https://idaas.iam.ibm.com/idaas/oidc/endpoint/default/authorize", | ||
"access_url": "https://idaas.iam.ibm.com/idaas/oidc/endpoint/default/token", | ||
"authorize_url": "https://login.ibm.com/oidc/endpoint/default/authorize", | ||
"access_url": "https://login.ibm.com/oidc/endpoint/default/token", | ||
"oauth": 2 | ||
@@ -562,8 +530,2 @@ }, | ||
"linkedin": { | ||
"request_url": "https://api.linkedin.com/uas/oauth/requestToken", | ||
"authorize_url": "https://www.linkedin.com/uas/oauth/authenticate", | ||
"access_url": "https://api.linkedin.com/uas/oauth/accessToken", | ||
"oauth": 1 | ||
}, | ||
"linkedin2": { | ||
"authorize_url": "https://www.linkedin.com/oauth/v2/authorization", | ||
@@ -614,4 +576,3 @@ "access_url": "https://www.linkedin.com/oauth/v2/accessToken", | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true | ||
"scope_delimiter": " " | ||
}, | ||
@@ -638,4 +599,3 @@ "medium": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["response_mode", "prompt", "login_hint", "domain_hint"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -682,4 +642,3 @@ "mixcloud": { | ||
"access_url": "https://api.nylas.com/oauth/token", | ||
"oauth": 2, | ||
"custom_parameters": ["login_hint"] | ||
"oauth": 2 | ||
}, | ||
@@ -690,9 +649,3 @@ "okta": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true, | ||
"custom_parameters": [ | ||
"code_challenge", "code_challenge_method", "idp_scope", "idp", | ||
"display", "login_hint", "max_age", "prompt", "response_mode", | ||
"sessionToken" | ||
] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -703,4 +656,3 @@ "onelogin": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true | ||
"scope_delimiter": " " | ||
}, | ||
@@ -716,4 +668,3 @@ "openstreetmap": { | ||
"access_url": "https://app.optimizely.com/oauth2/token", | ||
"oauth": 2, | ||
"custom_parameters": ["scopes"] | ||
"oauth": 2 | ||
}, | ||
@@ -767,4 +718,4 @@ "patreon": { | ||
"projectplace2": { | ||
"authorize_url": "https://service.projectplace.com/oauth2/authorize", | ||
"access_url": "https://service.projectplace.com/oauth2/access_token", | ||
"authorize_url": "https://api.projectplace.com/oauth2/authorize", | ||
"access_url": "https://api.projectplace.com/oauth2/access_token", | ||
"oauth": 2 | ||
@@ -780,4 +731,3 @@ }, | ||
"access_url": "https://graph.qq.com/oauth2.0/token", | ||
"oauth": 2, | ||
"custom_parameters": ["display"] | ||
"oauth": 2 | ||
}, | ||
@@ -799,4 +749,3 @@ "ravelry": { | ||
"access_url": "https://ssl.reddit.com/api/v1/access_token", | ||
"oauth": 2, | ||
"custom_parameters": ["duration"] | ||
"oauth": 2 | ||
}, | ||
@@ -822,4 +771,3 @@ "runkeeper": { | ||
"access_url": "https://[subdomain].myshopify.com/admin/oauth/access_token", | ||
"oauth": 2, | ||
"subdomain": true | ||
"oauth": 2 | ||
}, | ||
@@ -835,4 +783,3 @@ "skyrock": { | ||
"access_url": "https://slack.com/api/oauth.access", | ||
"oauth": 2, | ||
"custom_parameters": ["team"] | ||
"oauth": 2 | ||
}, | ||
@@ -854,7 +801,3 @@ "slice": { | ||
"access_url": "https://api.smugmug.com/services/oauth/1.0a/getAccessToken", | ||
"oauth": 1, | ||
"custom_parameters": [ | ||
"Access", "Permissions", "allowThirdPartyLogin", "showSignUpButton", | ||
"username", "viewportScale" | ||
] | ||
"oauth": 1 | ||
}, | ||
@@ -875,4 +818,3 @@ "snapchat": { | ||
"access_url": "https://[subdomain]/oauth/access_token", | ||
"oauth": 2, | ||
"subdomain": true | ||
"oauth": 2 | ||
}, | ||
@@ -882,4 +824,3 @@ "soundcloud": { | ||
"access_url": "https://api.soundcloud.com/oauth2/token", | ||
"oauth": 2, | ||
"custom_parameters": ["display"] | ||
"oauth": 2 | ||
}, | ||
@@ -890,4 +831,3 @@ "spotify": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["show_dialog"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -898,4 +838,3 @@ "square": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["locale", "session", "plan_id"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -921,4 +860,3 @@ "stackexchange": { | ||
"access_url": "https://www.strava.com/oauth/token", | ||
"oauth": 2, | ||
"custom_parameters": ["approval_prompt"] | ||
"oauth": 2 | ||
}, | ||
@@ -939,4 +877,3 @@ "stripe": { | ||
"access_url": "https://api.surveymonkey.net/oauth/token", | ||
"oauth": 2, | ||
"custom_parameters": ["api_key"] | ||
"oauth": 2 | ||
}, | ||
@@ -977,4 +914,3 @@ "thingiverse": { | ||
"access_url": "https://trello.com/1/OAuthGetAccessToken", | ||
"oauth": 1, | ||
"custom_parameters": ["name", "expiration"] | ||
"oauth": 1 | ||
}, | ||
@@ -997,4 +933,3 @@ "tripit": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"custom_parameters": ["force_verify"] | ||
"scope_delimiter": " " | ||
}, | ||
@@ -1044,4 +979,3 @@ "twitter": { | ||
"access_url": "https://[subdomain].vendhq.com/api/1.0/token", | ||
"oauth": 2, | ||
"subdomain": true | ||
"oauth": 2 | ||
}, | ||
@@ -1104,4 +1038,3 @@ "venmo": { | ||
"access_url": "https://public-api.wordpress.com/oauth2/token", | ||
"oauth": 2, | ||
"custom_parameters": ["blog"] | ||
"oauth": 2 | ||
}, | ||
@@ -1128,4 +1061,3 @@ "wrike": { | ||
"access_url": "https://api.login.yahoo.com/oauth2/get_token", | ||
"oauth": 2, | ||
"custom_parameters": ["language", "prompt", "max_age"] | ||
"oauth": 2 | ||
}, | ||
@@ -1140,4 +1072,3 @@ "yammer": { | ||
"access_url": "https://oauth.yandex.com/token", | ||
"oauth": 2, | ||
"custom_parameters": ["device_id", "device_name"] | ||
"oauth": 2 | ||
}, | ||
@@ -1153,4 +1084,3 @@ "zeit": { | ||
"oauth": 2, | ||
"scope_delimiter": " ", | ||
"subdomain": true | ||
"scope_delimiter": " " | ||
}, | ||
@@ -1157,0 +1087,0 @@ "zoom": { |
@@ -7,9 +7,18 @@ [ | ||
"scope_delimiter", | ||
"custom_parameters", | ||
"token_endpoint_auth_method", | ||
"origin", | ||
"prefix", | ||
"state", | ||
"nonce", | ||
"pkce", | ||
"response", | ||
"transport", | ||
"callback", | ||
"overrides", | ||
"dynamic", | ||
"protocol", | ||
"host", | ||
"path", | ||
"transport", | ||
"state", | ||
@@ -25,9 +34,2 @@ "key", | ||
"subdomain", | ||
"nonce", | ||
"pkce", | ||
"callback", | ||
"dynamic", | ||
"overrides", | ||
"response", | ||
"token_endpoint_auth_method", | ||
@@ -34,0 +36,0 @@ "name", |
43
grant.js
exports.express = () => { | ||
return require('./lib/consumer/express') | ||
function grant ({handler, ...rest}) { | ||
var version = () => ({ | ||
express: 4, | ||
koa: parseInt(require('koa/package.json').version.split('.')[0]) >= 2 ? 2 : 1, | ||
hapi: (() => { | ||
var pkg | ||
try { | ||
pkg = require('@hapi/hapi/package.json') | ||
} | ||
catch (err) { | ||
pkg = require('hapi/package.json') | ||
} | ||
return parseInt(pkg.version.split('.')[0]) | ||
})() >= 17 ? 17 : 16 | ||
}[handler]) | ||
if (/express|koa|hapi/.test(handler) && !/-\d+$/.test(handler)) { | ||
return require(`./lib/handler/${handler}-${version()}`)(rest) | ||
} | ||
else { | ||
return require(`./lib/handler/${handler}`)(rest) | ||
} | ||
} | ||
exports.koa = () => { | ||
grant.express = () => { | ||
var version = 4 | ||
return require(`./lib/handler/express-${version}`) | ||
} | ||
grant.koa = () => { | ||
var version = parseInt(require('koa/package.json').version.split('.')[0]) | ||
return require('./lib/consumer/koa' + (version < 2 ? '' : '2')) | ||
return require('./lib/handler/koa-' + (version >= 2 ? 2 : 1)) | ||
} | ||
exports.hapi = () => { | ||
grant.hapi = () => { | ||
var pkg | ||
try { | ||
pkg = require('hapi/package.json') | ||
pkg = require('@hapi/hapi/package.json') | ||
} | ||
catch (err) { | ||
pkg = require('@hapi/hapi/package.json') | ||
pkg = require('hapi/package.json') | ||
} | ||
var version = parseInt(pkg.version.split('.')[0]) | ||
return require('./lib/consumer/hapi' + (version < 17 ? '' : '17')) | ||
return require('./lib/handler/hapi-' + (version >= 17 ? 17 : 16)) | ||
} | ||
module.exports = grant |
@@ -24,7 +24,3 @@ | ||
// reserved key | ||
reserved.includes(key) || | ||
// custom parameter | ||
(obj.custom_parameters && obj.custom_parameters.includes(key)) || | ||
// static override | ||
typeof obj[key] === 'object' | ||
reserved.includes(key) | ||
)) | ||
@@ -77,9 +73,12 @@ .reduce((all, key) => (all[key] = obj[key], all), {}) | ||
redirect_uri: ({redirect_uri, protocol, host, path = '', name}) => | ||
redirect_uri: ({redirect_uri, origin, prefix, protocol, host, name}) => | ||
redirect_uri | ||
? redirect_uri | ||
: protocol && host && name | ||
? `${protocol}://${host}${path}/connect/${name}/callback` | ||
: origin | ||
? `${origin}${prefix}/${name}/callback` | ||
: protocol && host | ||
? `${protocol}://${host}${prefix}/${name}/callback` | ||
: undefined | ||
@@ -89,17 +88,4 @@ , | ||
custom_params: (provider) => { | ||
var params = provider.custom_params || {} | ||
var keys = (provider.custom_parameters || []) | ||
.filter((key) => | ||
!reserved.includes(key) && | ||
key !== provider.name && | ||
typeof provider[key] !== 'object' | ||
) | ||
// extract | ||
var direct = keys.reduce((all, key) => (all[key] = provider[key], all), {}) | ||
// merge | ||
var params = Object.assign(direct, provider.custom_params || {}) | ||
// remove | ||
keys.forEach((key) => delete provider[key]) | ||
// remove falsy | ||
@@ -114,23 +100,10 @@ params = Object.keys(params) | ||
overrides: (provider) => { | ||
var keys = Object.keys(provider) | ||
.filter((key) => | ||
!reserved.includes(key) && | ||
key !== provider.name && | ||
typeof provider[key] === 'object' | ||
) | ||
// extract | ||
var direct = keys.reduce((all, key) => (all[key] = provider[key], all), {}) | ||
// merge | ||
var overrides = Object.assign(direct, provider.overrides || {}) | ||
// remove | ||
keys.forEach((key) => delete provider[key]) | ||
var overrides = provider.overrides || {} | ||
delete provider.overrides | ||
// remove nested | ||
Object.keys(overrides).forEach((key) => { | ||
overrides[key] = Object.keys(overrides[key]) | ||
.filter((nested) => reserved.includes(nested) && nested !== 'overrides') | ||
.reduce((all, nested) => (all[nested] = overrides[key][nested], all), {}) | ||
Object.keys(overrides).forEach((name) => { | ||
overrides[name] = Object.keys(overrides[name]) | ||
.filter((key) => key !== 'overrides') | ||
.reduce((all, key) => (all[key] = overrides[name][key], all), {}) | ||
}) | ||
@@ -147,8 +120,11 @@ | ||
var state = (provider, key = 'state', value = provider[key]) => | ||
/string|number/.test(typeof value) | ||
value === true || value === 'true' | ||
? crypto.randomBytes(10).toString('hex') | ||
: value === 'false' | ||
? undefined | ||
: /string|number/.test(typeof value) | ||
? value.toString() | ||
: value === true | ||
? crypto.randomBytes(10).toString('hex') | ||
: undefined | ||
@@ -175,14 +151,20 @@ | ||
var compat = (config) => | ||
config.fitbit2 ? ( | ||
Object.assign({}, config, {fitbit2: Object.assign({}, oauth.fitbit, config.fitbit2)}) | ||
) : config | ||
config.fitbit2 ? (Object.assign({}, config, {fitbit2: Object.assign({}, oauth.fitbit, config.fitbit2)})) : | ||
config.linkedin2 ? (Object.assign({}, config, {linkedin2: Object.assign({}, oauth.fitbit, config.linkedin2)})) : | ||
config | ||
var defaults = ({path, prefix = '/connect', ...rest} = {}) => ({ | ||
...rest, | ||
prefix: path ? `${path}${prefix}` : prefix | ||
}) | ||
// init all configured providers | ||
var ctor = (config = {}, defaults = config.defaults || config.server) => | ||
var ctor = ((_defaults) => (config = {}, defaults = _defaults(config.defaults)) => | ||
Object.keys(compat(config)) | ||
.filter((name) => !/defaults|server/.test(name)) | ||
.filter((name) => !/defaults/.test(name)) | ||
.reduce((all, name) => ( | ||
all[name] = init(oauth[name], defaults, config[name], {name, [name]: true}), | ||
all | ||
), defaults ? {defaults} : {}) | ||
), {defaults}) | ||
)(defaults) | ||
@@ -241,3 +223,3 @@ // get provider on connect | ||
module.exports = Object.assign(ctor, { | ||
compose, dcopy, merge, filter, format, state, pkce, transform, init, compat, provider | ||
compose, dcopy, merge, filter, format, state, pkce, transform, init, defaults, compat, provider | ||
}) |
var qs = require('qs') | ||
var request = require('../client') | ||
var response = require('../response') | ||
exports.request = (provider) => new Promise((resolve, reject) => { | ||
exports.request = async ({provider, input}) => { | ||
var options = { | ||
@@ -37,16 +36,20 @@ method: 'POST', | ||
} | ||
request(options) | ||
.then(resolve) | ||
.catch((err) => reject({error: err.body || err.message})) | ||
}) | ||
try { | ||
var {body:output} = await request(options) | ||
} | ||
catch (err) { | ||
var output = {error: err.body || err.message} | ||
} | ||
return {provider, input, output} | ||
} | ||
exports.authorize = (provider, req) => new Promise((resolve, reject) => { | ||
if (!req.oauth_token && !req.code) { | ||
reject(Object.keys(req).length ? {error: req} | ||
: {error: {error: 'Grant: OAuth1 missing oauth_token parameter'}}) | ||
return | ||
exports.authorize = async ({provider, input, output}) => { | ||
if (!output.oauth_token && !output.code) { | ||
output = Object.keys(output).length | ||
? output : {error: 'Grant: OAuth1 missing oauth_token parameter'} | ||
return {provider, input, output} | ||
} | ||
var url = provider.authorize_url | ||
var params = { | ||
oauth_token: req.oauth_token | ||
oauth_token: output.oauth_token | ||
} | ||
@@ -63,3 +66,3 @@ if (provider.custom_params) { | ||
params = { | ||
request_token: req.code, | ||
request_token: output.code, | ||
redirect_uri: provider.redirect_uri | ||
@@ -77,10 +80,10 @@ } | ||
} | ||
resolve(`${url}?${qs.stringify(params)}`) | ||
}) | ||
return {provider, input, output: `${url}?${qs.stringify(params)}`} | ||
} | ||
exports.access = (provider, req, authorize) => new Promise((resolve, reject) => { | ||
if (!authorize.oauth_token && !req.code) { | ||
reject(Object.keys(authorize).length ? {error: authorize} | ||
: {error: 'Grant: OAuth1 missing oauth_token parameter'}) | ||
return | ||
exports.access = async ({provider, input, input:{session, query}}) => { | ||
if (!query.oauth_token && !session.request.code) { | ||
var output = Object.keys(query).length | ||
? query : {error: 'Grant: OAuth1 missing oauth_token parameter'} | ||
return {provider, input, output} | ||
} | ||
@@ -93,5 +96,5 @@ var options = { | ||
consumer_secret: provider.secret, | ||
token: authorize.oauth_token, | ||
token_secret: req.oauth_token_secret, | ||
verifier: authorize.oauth_verifier | ||
token: query.oauth_token, | ||
token_secret: session.request.oauth_token_secret, | ||
verifier: query.oauth_verifier | ||
} | ||
@@ -109,3 +112,3 @@ } | ||
consumer_key: provider.key, | ||
code: req.code | ||
code: session.request.code | ||
} | ||
@@ -119,10 +122,12 @@ } | ||
} | ||
request(options) | ||
.then(({body}) => { | ||
if (provider.intuit) { | ||
body.realmId = authorize.realmId | ||
} | ||
resolve(response(provider, body)) | ||
}) | ||
.catch((err) => reject({error: err.body || err.message})) | ||
}) | ||
try { | ||
var {body:output} = await request(options) | ||
if (provider.intuit) { | ||
output.realmId = query.realmId | ||
} | ||
} | ||
catch (err) { | ||
var output = {error: err.body || err.message} | ||
} | ||
return {provider, input, output} | ||
} |
@@ -5,6 +5,5 @@ | ||
var request = require('../client') | ||
var response = require('../response') | ||
exports.authorize = (provider) => new Promise((resolve) => { | ||
exports.authorize = async ({provider, input}) => { | ||
var url = provider.authorize_url | ||
@@ -60,14 +59,14 @@ var params = { | ||
} | ||
resolve(`${url}?${querystring}`) | ||
}) | ||
return {provider, input, output: `${url}?${querystring}`} | ||
} | ||
exports.access = (provider, authorize, session) => new Promise((resolve, reject) => { | ||
if (!authorize.code) { | ||
reject(Object.keys(authorize).length ? {error: authorize} | ||
: {error: {error: 'Grant: OAuth2 missing code parameter'}}) | ||
return | ||
exports.access = async ({provider, input, input:{query, session}}) => { | ||
if (!query.code) { | ||
var output = Object.keys(query).length | ||
? query : {error: 'Grant: OAuth2 missing code parameter'} | ||
return {provider, input, output} | ||
} | ||
else if ((authorize.state && session.state) && (authorize.state !== session.state)) { | ||
reject({error: {error: 'Grant: OAuth2 state mismatch'}}) | ||
return | ||
else if ((query.state && session.state) && (query.state !== session.state)) { | ||
var output = {error: 'Grant: OAuth2 state mismatch'} | ||
return {provider, input, output} | ||
} | ||
@@ -79,3 +78,3 @@ var options = { | ||
grant_type: 'authorization_code', | ||
code: authorize.code, | ||
code: query.code, | ||
client_id: provider.key, | ||
@@ -95,3 +94,3 @@ client_secret: provider.secret, | ||
options.qs = { | ||
code: authorize.code, | ||
code: query.code, | ||
client_id: provider.key, | ||
@@ -131,3 +130,3 @@ client_secret: provider.secret | ||
var hash = crypto.createHash('sha256') | ||
hash.update(provider.secret + '|' + authorize.code) | ||
hash.update(provider.secret + '|' + query.code) | ||
options.form.hash = hash.digest('hex') | ||
@@ -143,3 +142,3 @@ } | ||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
assertion: authorize.code, | ||
assertion: query.code, | ||
redirect_uri: provider.redirect_uri | ||
@@ -151,5 +150,9 @@ } | ||
} | ||
request(options) | ||
.then(({body}) => resolve(response(provider, body, session))) | ||
.catch((err) => reject({error: err.body || err.message})) | ||
}) | ||
try { | ||
var {body:output} = await request(options) | ||
} | ||
catch (err) { | ||
var output = {error: err.body || err.message} | ||
} | ||
return {provider, input, output} | ||
} |
var credentials = (provider, body) => { | ||
var qs = require('qs') | ||
var tokens = (provider, response) => { | ||
var data = {} | ||
if (provider.concur) { | ||
data.access_token = body.replace( | ||
data.access_token = response.replace( | ||
/[\s\S]+<Token>([^<]+)<\/Token>[\s\S]+/, '$1') | ||
data.refresh_token = body.replace( | ||
data.refresh_token = response.replace( | ||
/[\s\S]+<Refresh_Token>([^<]+)<\/Refresh_Token>[\s\S]+/, '$1') | ||
} | ||
else if (provider.getpocket) { | ||
data.access_token = body.access_token | ||
data.access_token = response.access_token | ||
} | ||
else if (provider.yammer) { | ||
data.access_token = body.access_token.token | ||
data.access_token = response.access_token.token | ||
} | ||
else if (provider.oauth === 1) { | ||
if (body.oauth_token) { | ||
data.access_token = body.oauth_token | ||
if (response.oauth_token) { | ||
data.access_token = response.oauth_token | ||
} | ||
if (body.oauth_token_secret) { | ||
data.access_secret = body.oauth_token_secret | ||
if (response.oauth_token_secret) { | ||
data.access_secret = response.oauth_token_secret | ||
} | ||
} | ||
else if (provider.oauth === 2) { | ||
if (body.id_token) { | ||
data.id_token = body.id_token | ||
if (response.id_token) { | ||
data.id_token = response.id_token | ||
} | ||
if (body.access_token) { | ||
data.access_token = body.access_token | ||
if (response.access_token) { | ||
data.access_token = response.access_token | ||
} | ||
if (body.refresh_token) { | ||
data.refresh_token = body.refresh_token | ||
if (response.refresh_token) { | ||
data.refresh_token = response.refresh_token | ||
} | ||
@@ -41,8 +44,9 @@ } | ||
var oidc = (provider, body, session) => { | ||
if (!/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(body.id_token)) { | ||
var oidc = (provider, session, response) => { | ||
if (!/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(response.id_token)) { | ||
return {error: 'Grant: OpenID Connect invalid id_token format'} | ||
} | ||
var [header, payload, signature] = body.id_token.split('.') | ||
var [header, payload, signature] = response.id_token.split('.') | ||
try { | ||
@@ -66,26 +70,57 @@ header = JSON.parse(Buffer.from(header, 'base64').toString('binary')) | ||
module.exports = (provider, body, session) => { | ||
var data = credentials(provider, body) | ||
var data = ({provider, input, input:{session}, output}) => { | ||
if (output.error) { | ||
return {provider, input, output} | ||
} | ||
if (data.id_token) { | ||
var jwt = oidc(provider, body, session) | ||
if (output.id_token) { | ||
var jwt = oidc(provider, session, output) | ||
if (jwt.error) { | ||
return jwt | ||
return {provider, input, output: jwt} | ||
} | ||
data.id_token = jwt | ||
} | ||
if (provider.response) { | ||
if (body.id_token) { | ||
data.id_token = body.id_token | ||
if (!provider.response) { | ||
var data = tokens(provider, output) | ||
data.raw = output | ||
} | ||
else { | ||
var data = {} | ||
var response = [].concat(provider.response) | ||
if (response.find((key) => /token/.test(key))) { | ||
data = tokens(provider, output) | ||
} | ||
if (jwt && [].concat(provider.response).includes('jwt')) { | ||
data.id_token_jwt = jwt | ||
if (response.includes('jwt') && jwt) { | ||
data.jwt = {id_token: jwt} | ||
} | ||
if (response.includes('raw')) { | ||
data.raw = output | ||
} | ||
} | ||
else { | ||
data.raw = body | ||
} | ||
return data | ||
return {provider, input, output: data} | ||
} | ||
var transport = ({provider, input, input:{params, state, session}, output}) => ({ | ||
location: | ||
(params.override !== 'callback' && !output.error) | ||
? output | ||
: (!provider.transport || provider.transport === 'querystring') | ||
? `${provider.callback || ''}?${qs.stringify(output)}` | ||
: provider.transport === 'session' | ||
? provider.callback | ||
: undefined, | ||
session: ( | ||
provider.transport === 'session' ? session.response = output : null, | ||
session | ||
), | ||
state: ( | ||
provider.transport === 'state' ? state.response = output : null, | ||
state | ||
), | ||
}) | ||
module.exports = {data, transport} |
{ | ||
"name": "grant", | ||
"version": "4.7.0", | ||
"description": "OAuth Middleware for Express, Koa and Hapi", | ||
"version": "5.0.0", | ||
"description": "OAuth Proxy", | ||
"keywords": [ | ||
@@ -12,2 +12,3 @@ "oauth", | ||
"authorization", | ||
"proxy", | ||
"middleware", | ||
@@ -26,39 +27,29 @@ "express", | ||
"dependencies": { | ||
"qs": "^6.9.1", | ||
"request-compose": "^1.2.1", | ||
"request-oauth": "0.0.3" | ||
"qs": "^6.9.3", | ||
"request-compose": "^2.0.0", | ||
"request-oauth": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.26.0", | ||
"babel-polyfill": "^6.26.0", | ||
"babel-preset-env": "^1.7.0", | ||
"@hapi/hapi": "^18.4.1", | ||
"@hapi/yar": "^9.2.1", | ||
"body-parser": "^1.19.0", | ||
"cookie-session": "^1.3.3", | ||
"coveralls": "^3.0.9", | ||
"coveralls": "^3.0.11", | ||
"express": "^4.17.1", | ||
"express-session": "^1.17.0", | ||
"hapi": "~16.3.0", | ||
"istanbul": "^1.1.0-alpha.1", | ||
"koa": "^1.7.0", | ||
"koa-bodyparser": "^2.5.0", | ||
"koa-convert": "^1.2.0", | ||
"koa-mount": "^1.3.1", | ||
"koa": "^2.11.0", | ||
"koa-bodyparser": "^4.2.1", | ||
"koa-mount": "^4.0.0", | ||
"koa-qs": "^2.0.0", | ||
"koa-session": "^4.8.1", | ||
"mocha": "^5.2.0", | ||
"request-cookie": "0.0.2", | ||
"request-logs": "^1.0.3", | ||
"yar": "^8.1.2" | ||
"koa-session": "^5.13.1", | ||
"mocha": "^7.1.1", | ||
"nyc": "^15.0.0", | ||
"request-cookie": "^1.0.0", | ||
"request-logs": "^2.0.0" | ||
}, | ||
"main": "./build/index.js", | ||
"main": "./grant.js", | ||
"files": [ | ||
"build/config/", | ||
"build/lib/", | ||
"build/grant.js", | ||
"build/index.js", | ||
"build/package.json", | ||
"config/", | ||
"lib/", | ||
"grant.js", | ||
"index.js", | ||
"CHANGELOG.md", | ||
@@ -71,10 +62,8 @@ "LICENSE", | ||
"test": "npm run test:ci", | ||
"test:ci": "v=$(node -e \"console.log(process.version.split('.')[0].slice(1))\") && if [ $v -ge 8 ]; then p=\"test\"; else p=\"build/test\"; fi && mocha $p --recursive", | ||
"test:cov": "istanbul cover _mocha -- --recursive", | ||
"build": "./build.sh", | ||
"prepublish": "npm run build" | ||
"test:ci": "npx mocha --recursive", | ||
"test:cov": "npx nyc --reporter=lcov --reporter=text-summary mocha -- --recursive" | ||
}, | ||
"engines": { | ||
"node": ">=4.0.0" | ||
"node": ">=8.0.0" | ||
} | ||
} |
851
README.md
@@ -6,2 +6,3 @@ | ||
> _OAuth Proxy_ | ||
@@ -15,40 +16,17 @@ ## 180+ Supported Providers / [OAuth Playground][grant-oauth] | ||
### [Migration Guide: from v4 to v5][migration] | ||
- **[Providers](#grant)** | ||
- **Middlewares** | ||
- [Express](#express) | ||
- [Koa](#koa) | ||
- [Hapi](#hapi) | ||
- **Basics** | ||
- [Configuration](#configuration) | ||
- [Reserved Routes](#reserved-routes) | ||
- [Redirect URL](#redirect-url) | ||
- [Path Prefix](#path-prefix) | ||
- [OpenID Connect](#openid-connect) | ||
- [Custom Parameters](#custom-parameters) | ||
- [Static Overrides](#static-overrides) | ||
- [Dynamic Override](#dynamic-override) | ||
- **Response Data** | ||
- [Response Data Format](#response-data) | ||
- [Response Data Transport](#response-data-transport) | ||
- [Limit Response Data](#limit-response-data) | ||
- [Session](#session) | ||
- **Advanced** | ||
- [All Available Options](#all-available-options) | ||
- [Configuration Scopes](#configuration-scopes) | ||
- [Custom Providers](#custom-providers) | ||
- [Development Environments](#development-environments) | ||
- [OAuth Proxy](#oauth-proxy) | ||
- [Redirect URI](#redirect-uri) | ||
- [Token Endpoint Auth Method](#token-endpoint-auth-method) | ||
- [Meta Configuration](#meta-configuration) | ||
- **OAuth Quirks** | ||
- [Subdomain URLs](#subdomain-urls) | ||
- [Sandbox OAuth URLs](#sandbox-oauth-urls) | ||
- [Sandbox Redirect URI](#sandbox-redirect-uri) | ||
- [Provider Quirks](#provider-quirks) | ||
- **Handlers** | ||
- [Express](#express) / [Koa](#koa) / [Hapi](#hapi) | ||
- **Configuration** | ||
- [Basics](#configuration-basics) / [Description](#configuration-description) / [Values](#configuration-values) / [Scopes](#configuration-scopes) | ||
- **Connect** | ||
- [Origin](#connect-origin) / [Prefix](#connect-prefix) / [Redirect URI](#connect-redirect-uri) / [Custom Parameters](#connect-custom-parameters) / [OpenID Connect](#connect-openid-connect) / [PKCE](#connect-pkce) / [Static Overrides](#connect-static-overrides) | ||
- **Callback** | ||
- [Data](#callback-data) / [Transport](#callback-transport) / [Response](#callback-response) / [Session](#callback-session) | ||
- **Dynamic Configuration** | ||
- [Instance](#dynamic-instance) / [State](#dynamic-state) / [HTTP](#dynamic-http) / [OAuth Proxy](#dynamic-oauth-proxy) | ||
- **Misc** | ||
- [Alternative Require](#alternative-require) | ||
- [Alternative Instantiation](#alternative-instantiation) | ||
- [Programmatic Access](#programmatic-access) | ||
- [Get User Profile](#get-user-profile) | ||
- [Configuration](#misc-redirect-uri) / [Middlewares](#misc-alternative-require) / [OAuth Quirks](#misc-oauth-quirks) | ||
- **[Examples][examples]** | ||
@@ -59,3 +37,3 @@ - **[Changelog][changelog]** | ||
# Middlewares | ||
# Handlers | ||
@@ -124,6 +102,6 @@ | ||
# Basics | ||
# Configuration | ||
## Configuration | ||
## Configuration: Basics | ||
@@ -133,4 +111,3 @@ ```json | ||
"defaults": { | ||
"protocol": "http", | ||
"host": "localhost:3000", | ||
"origin": "http://localhost:3000", | ||
"transport": "session", | ||
@@ -155,7 +132,6 @@ "state": true | ||
- **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](#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)* | ||
- **defaults** - default configuration for all providers | ||
- **origin** - where your client server can be reached `http://localhost:3000` | `https://site.com` ... | ||
- **transport** - a [transport](#callback-transport) to use to deliver the [response data](#callback-response) in your `callback` route | ||
- **state** - generate random state string on each authorization attempt | ||
- **provider** - any [supported provider](#grant) `google` | `twitter` ... | ||
@@ -165,55 +141,126 @@ - **key** - `consumer_key` or `client_id` of your OAuth app | ||
- **scope** - array of OAuth scopes to request | ||
- **nonce** - generate random nonce string on each authorization attempt `true` | `false` *([OpenID Connect](#openid-connect) only, defaults to false if omitted)* | ||
- **custom_params** - custom [authorization parameters](#custom-parameters) | ||
- **callback** - specific callback route to use for this provider only `/callback` | `/done` ... | ||
- **nonce** - generate random nonce string on each authorization attempt ([OpenID Connect](#connect-openid-connect) only) | ||
- **custom_params** - custom [authorization parameters](#connect-custom-parameters) | ||
- **callback** - relative route or absolute URL to receive the response data `/hello` | `https://site.com/hey` ... | ||
> List of all [available options](#all-available-options). | ||
## Configuration: Description | ||
## Reserved Routes | ||
Key | Location | Description | ||
:-| :-: | :- | ||
***Authorization Server*** | | ||
**`request_url`** | [oauth.json] | OAuth 1.0a only, first step | ||
**`authorize_url`** | [oauth.json] | OAuth 2.0 first step, OAuth 1.0a second step | ||
**`access_url`** | [oauth.json] | OAuth 2.0 second step, OAuth 1.0a third step | ||
**`oauth`** | [oauth.json] | OAuth version number | ||
**`scope_delimiter`** | [oauth.json] | String delimiter used for concatenating multiple scopes | ||
**`token_endpoint_auth_method`** | `[provider]` | Authentication method for the token endpoint | ||
***Client Server*** | | ||
**`origin`** | `defaults` | Where your server and Grant can be reached | ||
**`prefix`** | `defaults` | Path prefix for the Grant internal routes | ||
**`state`** | `defaults` | Random state string for OAuth2 | ||
**`nonce`** | `defaults` | Random nonce string for OpenID Connect | ||
**`pkce`** | `defaults` | PKCE support | ||
**`response`** | `defaults` | Response data to receive | ||
**`transport`** | `defaults` | A way to deliver the response data | ||
**`callback`** | `[provider]` | Relative or absolute URL to receive the response data | ||
**`overrides`** | `[provider]` | Static configuration overrides for a provider | ||
**`dynamic`** | `[provider]` | Configuration keys that can be overridden dynamically over HTTP | ||
***Client App*** | | ||
**`key`** | `[provider]` | The `consumer_key` or `client_id` of your OAuth app | ||
**`secret`** | `[provider]` | The `consumer_secret` or `client_secret` of your OAuth app | ||
**`scope`** | `[provider]` | List of scopes to request | ||
**`custom_params`** | `[provider]` | Custom authorization parameters and their values | ||
**`subdomain`** | `[provider]` | String to embed into the authorization server URLs | ||
**`redirect_uri`** | `generated` | Absolute redirect URL of the OAuth app | ||
***Grant*** | | ||
**`name`** | `generated` | Provider's [name](#grant) | ||
**`[provider]`** | `generated` | Provider's [name](#grant) as key | ||
Grant operates on the following two routes: | ||
``` | ||
/connect/:provider/:override? | ||
/connect/:provider/callback | ||
``` | ||
## Configuration: Values | ||
You login by navigating to the `/connect/:provider` route where `:provider` is a key in your configuration, usually one of the [officially supported](#grant) providers. Additionally you can login through a [static override](#static-overrides) key defined for that provider, in your configuration, by navigating to the `/connect/:provider/:override?` route. | ||
Key | Location | Value | ||
:- | :-: | :-: | ||
***Authorization Server*** | | ||
**`request_url`** | [oauth.json] | `'https://api.twitter.com/oauth/request_token'` | ||
**`authorize_url`** | [oauth.json] | `'https://api.twitter.com/oauth/authenticate'` | ||
**`access_url`** | [oauth.json] | `'https://api.twitter.com/oauth/access_token'` | ||
**`oauth`** | [oauth.json] | `2` `1` | ||
**`scope_delimiter`** | [oauth.json] | `','` `' '` | ||
**`token_endpoint_auth_method`** | [oauth.json] | `'client_secret_post'` `'client_secret_basic'` | ||
***Client Server*** | | ||
**`origin`** | `defaults` | `'http://localhost:3000'` `https://site.com` | ||
**`prefix`** | `defaults` | `'/connect'` `/oauth` `''` | ||
**`state`** | `defaults` | `true` | ||
**`nonce`** | `defaults` | `true` | ||
**`pkce`** | `defaults` | `true` | ||
**`response`** | `defaults` | `['tokens', 'raw', 'jwt']` | ||
**`transport`** | `defaults` | `'querystring'` `'session'` `'state'` | ||
**`callback`** | `[provider]` | `'/hello'` `'https://site.com/hey'` | ||
**`overrides`** | `[provider]` | `{something: {scope: ['..']}}` | ||
**`dynamic`** | `[provider]` | `['scope', 'subdomain']` | ||
***Client App*** | | ||
**`key`** | `[provider]` | `'123'` | ||
**`secret`** | `[provider]` | `'123'` | ||
**`scope`** | `[provider]` | `['openid', '..']` | ||
**`custom_params`** | `[provider]` | `{access_type: 'offline'}` | ||
**`subdomain`** | `[provider]` | `'myorg'` | ||
**`redirect_uri`** |`generated` | `'http://localhost:3000/connect/twitter/callback'` | ||
***Grant*** | | ||
**`name`** |`generated` | `name: 'twitter'` | ||
**`[provider]`** |`generated` | `twitter: true` | ||
## Redirect URL | ||
## Configuration: Scopes | ||
You should **always** use the following format for the `redirect_uri` of your OAuth App: | ||
Grant relies on configuration gathered from **6** different places: | ||
``` | ||
[protocol]://[host]/connect/[provider]/callback | ||
``` | ||
1. The **first** place Grant looks for configuration is the built-in [oauth.json] file located in the config folder. | ||
The `protocol` and the `host` are the options you specify inside the `defaults` key of your [configuration](#configuration), and this is where your server accepts incoming requests. The `provider` is a key in your configuration, usually one of the [officially supported](#grant) providers. | ||
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. | ||
Note that the `/connect/:provider/callback` route is used **internally** by Grant! You will receive the OAuth [response data](#response-data) inside the `callback` route specified in your 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 are the provider's [`overrides`](#connect-static-overrides). The static overrides inherit their parent provider, essentially creating new provider of the same type. | ||
## Path Prefix | ||
5. The **fifth** place for configuration is the dynamic [`state`](#dynamic-state) override. The request/response lifecycle state of your HTTP framework of choice can be used to dynamically override configuration. | ||
You can mount Grant under specific path prefix: | ||
6. The **sixth** place for configuration, that _[potentially](#dynamic-oauth-proxy)_ can override all of the above, and make all of the above optional, is the [`dynamic`](#dynamic-http) HTTP override. | ||
```js | ||
// 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)}]) | ||
--- | ||
# Connect | ||
## Connect: Origin | ||
```json | ||
{ | ||
"defaults": { | ||
"origin": "http://localhost:3000" | ||
} | ||
} | ||
``` | ||
In this case it is **required** to specify that `path` prefix in your configuration: | ||
The `origin` is where your client server is listening to and can be reached. | ||
You login by navigating to the `/connect/:provider` route where `:provider` is a key in your configuration, usually one of the [officially supported](#grant) ones, but you can define [your own](#misc-custom-providers) as well. Additionally you can login through a [static override](#connect-static-overrides) defined for that provider by navigating to the `/connect/:provider/:override?` route. | ||
## Connect: Prefix | ||
By default Grant operates on the following two routes: | ||
``` | ||
/connect/:provider/:override? | ||
/connect/:provider/callback | ||
``` | ||
However, the default `/connect` prefix can be configured: | ||
```json | ||
{ | ||
"defaults": { | ||
"protocol": "...", | ||
"host": "...", | ||
"path": "/path/prefix" | ||
"origin": "http://localhost:3000", | ||
"prefix": "/oauth" | ||
} | ||
@@ -223,14 +270,32 @@ } | ||
That path prefix **should** also be specified in your OAuth App [Redirect URL](#redirect-url): | ||
## Connect: Redirect URI | ||
The [`redirect_uri`](#misc-redirect-uri) of your OAuth app should follow this format: | ||
``` | ||
[protocol]://[host][path]/connect/[provider]/callback | ||
[origin][prefix]/[provider]/callback | ||
``` | ||
Optionally you can prefix your `callback` routes as well: | ||
Where [`origin`](#connect-origin) and [`prefix`](#connect-prefix) have to match the ones set in your configuration, and [`provider`](#grant) is a provider key found in your configuration. | ||
For example: `http://localhost:3000/connect/google/callback` | ||
This redirect URI is used internally by Grant. Depending on the [`transport`](#callback-transport) being used you will receive the response data in the [`callback`](#callback-data) route or absolute URL configured for that provider. | ||
## Connect: Custom Parameters | ||
Some providers may employ custom authorization parameters, that you can configure using the `custom_params` option: | ||
```json | ||
{ | ||
"github": { | ||
"callback": "/path/prefix/hello" | ||
"google": { | ||
"custom_params": {"access_type": "offline", "prompt": "consent"} | ||
}, | ||
"reddit": { | ||
"custom_params": {"duration": "permanent"} | ||
}, | ||
"trello": { | ||
"custom_params": {"name": "my app", "expiration": "never"} | ||
} | ||
@@ -241,5 +306,5 @@ } | ||
## OpenID Connect | ||
## Connect: OpenID Connect | ||
The `nonce` option is **recommended** when requesting the `openid` scope: | ||
The `openid` scope is required, and the `nonce` is optional but recommended: | ||
@@ -255,5 +320,5 @@ ```json | ||
Grant **does not** verify the signature of the returned `id_token` because that requires discovery, caching, and expiration of the provider's public keys. | ||
Grant **does not** verify the signature of the returned `id_token` by default. | ||
However, Grant tries to decode the `id_token` and verifies the following two claims *(returns error respectively)*: | ||
However, the following two claims of the `id_token` are being validated: | ||
@@ -263,21 +328,11 @@ 1. `aud` - is the token intended for my OAuth app? | ||
For convenience the [response data](#response-data) contains the decoded `id_token` | ||
> Take a look at the OpenID Connect [example][openid-connect-example]. | ||
## Connect: PKCE | ||
PKCE can be enabled for all providers or for a specific provider only: | ||
## Custom Parameters | ||
Some providers may employ custom authorization parameters, that you can configure using the `custom_params` option: | ||
```json | ||
{ | ||
"google": { | ||
"custom_params": {"access_type": "offline"} | ||
}, | ||
"reddit": { | ||
"custom_params": {"duration": "permanent"} | ||
}, | ||
"trello": { | ||
"custom_params": {"name": "my app", "expiration": "never"} | ||
"pkce": true | ||
} | ||
@@ -287,7 +342,9 @@ } | ||
Providers that do not support PKCE will ignore the additional parameters being sent. | ||
## Static Overrides | ||
You can specify provider sub configurations using the `overrides` key: | ||
## Connect: Static Overrides | ||
Provider sub configurations can be configured using the `overrides` key: | ||
```json | ||
@@ -306,3 +363,3 @@ { | ||
"scope": ["repo", "gist", "user"], | ||
"callback": "/awesome" | ||
"callback": "/hey" | ||
} | ||
@@ -316,81 +373,24 @@ } | ||
- `/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` 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 `scope`s and also receive the response data in another `callback` route | ||
--- | ||
## Dynamic Override | ||
# Callback | ||
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](#subdomain-urls) to be set dynamically: | ||
## Callback: Data | ||
```json | ||
{ | ||
"shopify": { | ||
"dynamic": ["subdomain"] | ||
} | ||
} | ||
``` | ||
By default the response data is returned in your `callback` route or absolute URL encoded as querystring. | ||
Then you can have a form on your website allowing the user to specify the shop name: | ||
Depending on the [`transport`](#callback-transport) being used the response data can also be returned in the `session` or in the `state` object. | ||
```html | ||
<form action="/connect/shopify" method="POST" accept-charset="utf-8"> | ||
<input type="text" name="subdomain" value="" /> | ||
<button>Login</button> | ||
</form> | ||
``` | ||
The amount of the returned data can also be controlled using the [`response`](#callback-response) option. | ||
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: | ||
### OAuth 2.0 | ||
```js | ||
// 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 | ||
``` | ||
Lastly you can use the request/response lifecycle state as well: | ||
```js | ||
// Express | ||
res.locals.grant = {dynamic: {subdomain: 'usershop'}} | ||
// Koa | ||
ctx.state.grant = {dynamic: {subdomain: 'usershop'}} | ||
// Hapi | ||
request.plugins.grant = {dynamic: {subdomain: 'usershop'}} | ||
``` | ||
Note that the dynamic overrides set in the request/response lifecycle state are not controlled by the `dynamic` configuration option. | ||
Any allowed dynamic key sent through GET/POST request will override the identical one set in state. | ||
--- | ||
# 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](#response-data-transport) instead, by using the `transport` option. | ||
- You can **limit** the amount of [returned data](#limit-response-data) by using the `response` option. | ||
## OAuth 2.0 | ||
```js | ||
{ | ||
id_token: {header: {...}, payload: {...}, signature: '...'}, | ||
id_token: '...', | ||
access_token: '...', | ||
@@ -407,6 +407,6 @@ refresh_token: '...', | ||
> The `refresh_token` is optional. The `id_token` is returned only for [OpenID Connect](#openid-connect) providers requesting the _openid_ scope. | ||
The `refresh_token` is optional. The `id_token` is returned only for [OpenID Connect](#connect-openid-connect) providers requesting the `openid` scope. | ||
## OAuth 1.0a | ||
### OAuth 1.0a | ||
@@ -426,3 +426,3 @@ ```js | ||
## Error | ||
### Error | ||
@@ -437,8 +437,8 @@ ```js | ||
> In case of an error, the `error` key will contain the error data. | ||
## Callback: Transport | ||
## Response Data Transport | ||
### querystring | ||
By default Grant will encode the OAuth [response data](#response-data) as *querystring* in your final `callback` route: | ||
By default Grant will encode the OAuth [response data](#callback-data) as `querystring` in your `callback` route or absolute URL: | ||
@@ -448,3 +448,3 @@ ```json | ||
"github": { | ||
"callback": "/hello" | ||
"callback": "https://site.com/hello" | ||
} | ||
@@ -454,6 +454,8 @@ } | ||
This final `/hello?access_token=...` redirect potentially may leak private data in your server logs, especially if you are behind reverse proxy. | ||
This is useful when using Grant as [OAuth Proxy](#dynamic-oauth-proxy). However this final `https://site.com/hello?access_token=...` redirect can potentially leak private data in your server logs, especially when sitting behind reverse proxy. | ||
It is **recommended** to use the *session* `transport` instead: | ||
### session | ||
For local `callback` routes the session `transport` is recommended: | ||
```json | ||
@@ -470,6 +472,14 @@ { | ||
That way the result will no longer be encoded as *querystring*, and you will receive the response data inside the [*session*][session-transport-example] instead. | ||
This will make the OAuth [response data](#callback-data) available in the `session` object instead: | ||
Lastly the request/response lifecycle state can be used as `state` transport: | ||
```js | ||
req.session.grant.response // Express | ||
ctx.session.grant.response // Koa | ||
req.yar.get('grant').response // Hapi | ||
``` | ||
### state | ||
Lastly the request/response lifecycle `state` can be used as well: | ||
```json | ||
@@ -483,21 +493,17 @@ { | ||
Note that in this case a `callback` route is not needed, and if there is one, the user won't be redirected there. The response data will be available in the request/response lifecycle state instead: | ||
In this case `callback` route is not needed, and it will be ignored if provided. The response data will be available in the request/response lifecycle `state` instead: | ||
```js | ||
// Express | ||
res.locals.grant.response | ||
// Koa | ||
ctx.state.grant.response | ||
// Hapi | ||
request.plugins.grant.response | ||
res.locals.grant.response // Express | ||
ctx.state.grant.response // Koa | ||
req.plugins.grant.response // Hapi | ||
``` | ||
## Callback: Response | ||
## Limit Response Data | ||
By default Grant returns all of the available tokens and the `raw` response data returned from the Authorization server: | ||
By default Grant will return all available [response data](#response-data) in your final `callback` route: | ||
```js | ||
{ | ||
id_token: {header: {...}, payload: {...}, signature: '...'}, | ||
id_token: '...', | ||
access_token: '...', | ||
@@ -514,12 +520,10 @@ refresh_token: '...', | ||
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. | ||
### querystring | ||
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. | ||
When using the querystring [`transport`](#callback-transport) it might be a good idea to limit the response data: | ||
The `response` option can be used to limit the [response data](#response-data): | ||
```json | ||
{ | ||
"defaults": { | ||
"response": "tokens" | ||
"response": ["tokens"] | ||
} | ||
@@ -529,10 +533,16 @@ } | ||
This will return only the tokens, without the `raw` key. | ||
This will return only the tokens available, without the `raw` response data. | ||
In case you want to include the decoded `id_token` as well: | ||
This is useful when using Grant as [OAuth Proxy](#dynamic-oauth-proxy). 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. | ||
### session | ||
Using the session [`transport`](#callback-transport) is generally safer, but it also depends on the implementation of your session store. | ||
In case your session store encodes the entire session in a cookie, not just the session ID, some servers may reject the HTTP request because of HTTP headers size being too big. | ||
```json | ||
{ | ||
"google": { | ||
"response": ["tokens", "jwt"] | ||
"response": ["tokens"] | ||
} | ||
@@ -542,11 +552,41 @@ } | ||
This will make the decoded `id_token` available as `id_token_jwt` in the response object. | ||
### jwt | ||
## Session | ||
Grant can also return even larger [response data](#callback-data) by including the decoded JWT for [OpenID Connect](#connect-openid-connect) providers that return `id_token`: | ||
```json | ||
{ | ||
"google": { | ||
"response": ["tokens", "raw", "jwt"] | ||
} | ||
} | ||
``` | ||
This will make the decoded JWT available in the response data: | ||
```js | ||
{ | ||
id_token: '...', | ||
access_token: '...', | ||
refresh_token: '...', | ||
raw: { | ||
id_token: '...', | ||
access_token: '...', | ||
refresh_token: '...', | ||
some: 'other data' | ||
} | ||
jwt: {id_token: {header: {}, payload: {}, signature: '...'}} | ||
} | ||
``` | ||
Make sure you include all response keys that you want returned when configuring the `response` data explicitly. | ||
## Callback: 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](#response-data) in your final `callback` route you are free to destroy that session, and do whatever you want with the returned data. | ||
Once you receive the [response data](#callback-data), in your `callback` route you are free to destroy that session. | ||
However, there are a few session keys returned in your final `callback` route, that you may find useful: | ||
However, there are a few session keys returned in your `callback` route, that you may find useful: | ||
@@ -556,78 +596,53 @@ Key | Availability | Description | ||
`provider` | **Always** | The provider [name](#grant) this authorization was called for | ||
`override` | Depends on URL | The [static override](#static-overrides) name used for this authorization | ||
`dynamic` | Depends on request type | The [dynamic override](#dynamic-override) configuration passed for this authorization | ||
`override` | Depends on URL | The [static override](#connect-static-overrides) name used for this authorization | ||
`dynamic` | Depends on request type | The [dynamic override](#dynamic-http) configuration passed for this authorization | ||
`state` | OAuth 2.0 only | OAuth 2.0 state string that was generated | ||
`nonce` | OpenID Connect only | [OpenID Connect](#openid-connect) nonce string that was generated | ||
`code_verifier` | PKCE only | The code verifier that was generated | ||
`nonce` | OpenID Connect only | [OpenID Connect](#connect-openid-connect) nonce string that was generated | ||
`code_verifier` | PKCE only | The code verifier that was generated for [PKCE](#connect-pkce) | ||
`request` | OAuth 1.0a only | Data returned from the first request of the OAuth 1.0a flow | ||
`response` | Depends on transport used | The final [response data](#response-data) | ||
`response` | Depends on transport used | The final [response data](#callback-data) | ||
--- | ||
# Advanced | ||
# Dynamic Configuration | ||
## All Available Options | ||
## Dynamic: Instance | ||
Key | Location | Description | ||
:---| :--- | :--- | ||
request_url | [oauth.json][oauth-config] | OAuth1/step1 | ||
authorize_url | [oauth.json][oauth-config] | OAuth1/step2 or OAuth2/step1 | ||
access_url | [oauth.json][oauth-config] | OAuth1/step3 or OAuth2/step2 | ||
oauth | [oauth.json][oauth-config] | OAuth version number | ||
scope_delimiter | [oauth.json][oauth-config] | string delimiter used for concatenating multiple scopes | ||
protocol, host, path | `defaults` | used to generate `redirect_uri` | ||
transport | `defaults` | [transport](#response-data-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](#custom-parameters) and their values | ||
subdomain | `[provider]` | string to be [embedded](#subdomain-urls) in `request_url`, `authorize_url` and `access_url` | ||
nonce | `[provider]` | toggle random `nonce` string generation for [OpenID Connect](#openid-connect) providers | ||
pkce | `[provider]` | toggle `pkce` support | ||
callback | `[provider]` | final callback route on your server to receive the [response data](#response-data) | ||
dynamic | `[provider]` | allow [dynamic override](#dynamic-override) of configuration | ||
overrides | `[provider]` | [static overrides](#static-overrides) for a provider | ||
response | `[provider]` | [limit](#limit-response-data) the response data | ||
token_endpoint_auth_method | `[provider]` | authentication method for the [token endpoint](#token-endpoint-auth-method) | ||
name | generated | provider's [name](#grant), used to generate `redirect_uri` | ||
[provider] | generated | provider's [name](#grant) as key | ||
redirect_uri | generated | OAuth app [redirect URI](#redirect-uri), generated using `protocol`, `host`, `path` and `name` | ||
Every Grant instance have a `config` property attached to it: | ||
```js | ||
var grant = Grant(require('./config')) | ||
console.log(grant.config) | ||
``` | ||
## Configuration Scopes | ||
You can use the `config` property to alter the Grant's behavior during runtime without having to restart your server. | ||
Grant relies on configuration gathered from **6** different places: | ||
Keep in mind that this property contains the **generated** configuration that Grant uses internally, and changes to that configuration affects the **entire** Grant instance! | ||
1. The **first** place Grant looks for configuration is the built-in [oauth.json][oauth-config] 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. | ||
## Dynamic: State | ||
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][oauth-config] file, and all options defined inside the `defaults` key. Having [oauth.json][oauth-config] file and a `defaults` configuration is only a convenience. You can define all available options directly for a provider. | ||
The request/response lifecycle state can be used to alter your configuration on every request: | ||
4. The **fourth** place for configuration are the provider's `overrides`. The [static overrides](#static-overrides) inherit their parent provider, essentially creating a sub provider of the same type. | ||
```js | ||
res.locals.grant = {dynamic: {subdomain: 'usershop'}} // Express | ||
ctx.state.grant = {dynamic: {subdomain: 'usershop'}} // Koa | ||
request.plugins.grant = {dynamic: {subdomain: 'usershop'}} // Hapi | ||
``` | ||
5. The **fifth** place for configuration is the [dynamic state override](#dynamic-override). The request/response lifecycle state of your HTTP framework of choice can be used to dynamically override configuration. | ||
Note that the request/response lifecycle `state` is not controlled by the [`dynamic`](#dynamic-http) configuration, meaning that you can override any configuration key. | ||
6. The **sixth** place for configuration, that _[potentially](#oauth-proxy)_ can override all of the above, and make all of the above optional, is the [dynamic HTTP override](#dynamic-override). | ||
Any allowed [`dynamic`](#dynamic-http) configuration key sent through HTTP GET/POST request will override the identical one set in `state`. | ||
## Dynamic: HTTP | ||
## Custom Providers | ||
The `dynamic` configuration allows certain configuration keys to be set dynamically over HTTP GET/POST request. | ||
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: | ||
For example `shopify` requires your shop name to be embedded into the OAuth URLs, so it makes sense to allow the [`subdomain`](#subdomain-urls) configuration key to be set dynamically: | ||
```json | ||
{ | ||
"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"] | ||
"shopify": { | ||
"dynamic": ["subdomain"] | ||
} | ||
@@ -637,52 +652,36 @@ } | ||
> Take a look at the [oauth.json][oauth-config] file to see how various providers are configured. | ||
Then you can have a web form in your website allowing the user to specify the shop name: | ||
```html | ||
<form action="/connect/shopify" method="POST" accept-charset="utf-8"> | ||
<input type="text" name="subdomain" value="" /> | ||
<button>Login</button> | ||
</form> | ||
``` | ||
## Development Environments | ||
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: | ||
You can easily configure different development environments: | ||
```json | ||
{ | ||
"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" | ||
} | ||
} | ||
} | ||
```js | ||
// 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)) | ||
``` | ||
Then you can pass the environment flag: | ||
Alternatively you can make a `GET` request to the `/connect/:provider/:override?` route: | ||
```bash | ||
NODE_ENV=production node app.js | ||
``` | ||
https://awesome.com/connect/shopify?subdomain=usershop | ||
``` | ||
And use it in your application: | ||
Note that `dynamic` configuration sent over HTTP GET/POST request override any other configuration. | ||
```js | ||
var config = require('./config.json') | ||
var grant = Grant(config[process.env.NODE_ENV || 'development']) | ||
``` | ||
## Dynamic: OAuth Proxy | ||
## OAuth Proxy | ||
In case you really want to, you can allow `dynamic` configuration override of every configuration key for a provider: | ||
In case you really want to, you can allow [dynamic override](#dynamic-override) of every option for a provider: | ||
```json | ||
@@ -706,14 +705,16 @@ { | ||
> Essentially Grant is a completely transparent **[OAuth Proxy][oauth-like-a-boss]**. | ||
Essentially Grant is a completely transparent **[OAuth Proxy][oauth-like-a-boss]**. | ||
--- | ||
## Redirect URI | ||
# Misc | ||
The `protocol`, the `host` (and optionally the `path`) options are used to generate **[the correct](#redirect-url)** `redirect_uri` for each provider: | ||
## Misc: Redirect URI | ||
The [`origin`](#connect-origin) and the [`prefix`](#connect-prefix) configuration is used to generate the correct [`redirect_uri`](#connect-redirect-uri) that Grant expects: | ||
```json | ||
{ | ||
"defaults": { | ||
"protocol": "https", | ||
"host": "awesome.com" | ||
"origin": "https://mysite.com" | ||
}, | ||
@@ -730,6 +731,6 @@ "google": {}, | ||
"google": { | ||
"redirect_uri": "https://awesome.com/connect/google/callback" | ||
"redirect_uri": "https://mysite.com/connect/google/callback" | ||
}, | ||
"twitter": { | ||
"redirect_uri": "https://awesome.com/connect/twitter/callback" | ||
"redirect_uri": "https://mysite.com/connect/twitter/callback" | ||
} | ||
@@ -739,22 +740,30 @@ } | ||
> Note that the `redirect_uri` option would override the `protocol` and the `host` even if they were specified. | ||
Note that explicitly specifying the `redirect_uri` overrides the one generated by default. | ||
## Token Endpoint Auth Method | ||
## Misc: Custom Providers | ||
Grant is handling the OAuth 2.0 _token endpoint_ request internally. | ||
You can define your own provider by adding a key for it in your configuration. In this case all of the required configuration keys have to be specified: | ||
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: | ||
```json | ||
"google": { | ||
"token_endpoint_auth_method": "client_secret_basic" | ||
{ | ||
"defaults": { | ||
"origin": "http://localhost:3000" | ||
}, | ||
"awesome": { | ||
"authorize_url": "https://awesome.com/authorize", | ||
"access_url": "https://awesome.com/token", | ||
"oauth": 2, | ||
"key": "...", | ||
"secret": "...", | ||
"scope": ["read", "write"] | ||
} | ||
} | ||
``` | ||
Take a look at the [oauth.json] file on how various providers are being configured. | ||
## Meta Configuration | ||
## Misc: Meta Configuration | ||
You can document your configuration by adding custom keys to it: | ||
@@ -774,16 +783,86 @@ | ||
> In the example above `meta` is an arbitrary key, but it cannot be one of the [reserved keys][reserved-keys]. | ||
Note that `meta` is an arbitrary key, but it cannot be one of the [reserved keys][reserved-keys]. | ||
--- | ||
# OAuth Quirks | ||
## Misc: Alternative Require | ||
All middlewares can be required directly from `grant` (each pair is identical): | ||
## Subdomain URLs | ||
```js | ||
// 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() | ||
``` | ||
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: | ||
## Misc: Alternative Instantiation | ||
Grant can be instantiated with or without using the `new` keyword: | ||
```js | ||
var Grant = require('grant-express|koa|hapi') | ||
// without using `new` | ||
var grant = Grant(config) | ||
// identical to | ||
var grant = new Grant(config) | ||
``` | ||
Additionally Hapi accepts the configuration in two different ways: | ||
```js | ||
server.register([{plugin: grant(config)}]) | ||
// identical to | ||
server.register([{plugin: grant(), options: config}]) | ||
``` | ||
## Misc: Path Prefix | ||
You can mount Grant under specific path prefix: | ||
```js | ||
app.use('/oauth', grant(config)) // Express | ||
app.use(mount('/oauth', grant(config))) // Koa - using koa-mount | ||
server.register([{routes: {prefix: '/oauth'}, plugin: grant(config)}]) // Hapi | ||
``` | ||
In this case the [`prefix`](#connect-prefix) configuration should reflect that + any other path parts that you may have: | ||
```json | ||
{ | ||
"defaults": { | ||
"origin": "http://localhost:3000", | ||
"prefix": "/oauth/login" | ||
} | ||
} | ||
``` | ||
In this case you login by navigating to: `http://localhost:3000/oauth/login/:provider` | ||
And the [`redirect_uri`](#connect-redirect-uri) of your OAuth app should be `http://localhost:3000/oauth/login/:provider/callback` | ||
Optionally you can prefix your [`callback`](#callback) routes as well: | ||
```json | ||
{ | ||
"github": { | ||
"callback": "/oauth/login/hello" | ||
} | ||
} | ||
``` | ||
## Misc: OAuth Quirks | ||
### Subdomain URLs | ||
Some providers have dynamic URLs containing bits of user information embedded into them. Inside the main [oauth.json] configuration file such URLs contain a `[subdomain]` token embedded in them. | ||
The `subdomain` option can be used to specify your company name, server region etc: | ||
```json | ||
"shopify": { | ||
@@ -810,8 +889,8 @@ "subdomain": "mycompany" | ||
> Alternatively you can override the entire `authorize_url` and `access_url` in your configuration. | ||
Alternatively you can override the entire `authorize_url` and `access_url` in your configuration. | ||
## Sandbox OAuth URLs | ||
### 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)*: | ||
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): | ||
@@ -831,9 +910,9 @@ ```json | ||
## Sandbox Redirect URI | ||
### Sandbox Redirect URI | ||
Very rarely you may need to override the `redirect_uri` that Grant [generates for you](#redirect-uri). | ||
Very rarely you may need to override the [`redirect_uri`](#connect-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: | ||
For example Feedly supports only `http://localhost` as redirect URI of their Sandbox OAuth app, and it won't allow the correct `http://localhost/connect/feedly/callback` URL: | ||
```js | ||
```json | ||
"feedly": { | ||
@@ -844,3 +923,3 @@ "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: | ||
In this case you'll have to redirect the user to the `[origin][prefix]/[provider]/callback` route that Grant uses to execute the last step of the OAuth flow: | ||
@@ -861,6 +940,6 @@ ```js | ||
> As usual you will receive the [response data](#response-data) in your final `callback` route. | ||
As usual you will receive the response data in your final [`callback`](#callback) route. | ||
## Provider Quirks | ||
### Provider Quirks | ||
@@ -870,3 +949,3 @@ | ||
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: | ||
Set the Redirect URI of your OAuth app as usual `[origin][prefix]/[provider]/callback`. Then Ebay will generate a special string called RuName (eBay Redirect URL name) that you need to set as `redirect_uri` in Grant: | ||
@@ -899,3 +978,3 @@ ```json | ||
Mastodon requires the entire *domain* of your server to be embedded in the OAuth URLs. However you should use the `subdomain` option: | ||
Mastodon requires the entire domain of your server to be embedded in the OAuth URLs. However you should use the `subdomain` option: | ||
@@ -922,16 +1001,2 @@ ```json | ||
> **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: | ||
```js | ||
"linkedin2": { | ||
// navigate to /connect/linkedin2 | ||
}, | ||
"projectplace2": { | ||
// navigate to /connect/projectplace2 | ||
} | ||
``` | ||
> **VisualStudio** | ||
@@ -950,94 +1015,3 @@ | ||
# Misc | ||
## Alternative Require | ||
Alternatively you can require any of the middlewares directly from `grant` *(each pair is identical)*: | ||
```js | ||
// 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: | ||
```js | ||
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: | ||
```js | ||
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: | ||
```js | ||
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](#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: | ||
```js | ||
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) | ||
``` | ||
[npm-version]: https://img.shields.io/npm/v/grant.svg?style=flat-square (NPM Version) | ||
@@ -1054,9 +1028,6 @@ [travis-ci]: https://img.shields.io/travis/simov/grant/master.svg?style=flat-square (Build Status) | ||
[oauth-config]: https://github.com/simov/grant/blob/master/config/oauth.json | ||
[oauth.json]: https://github.com/simov/grant/blob/master/config/oauth.json | ||
[reserved-keys]: https://github.com/simov/grant/blob/master/config/reserved.json | ||
[examples]: https://github.com/simov/grant/tree/master/examples#table-of-contents | ||
[changelog]: https://github.com/simov/grant/blob/master/CHANGELOG.md | ||
[session-transport-example]: https://github.com/simov/grant/blob/master/examples/session-transport | ||
[oauth-proxy-example]: https://github.com/simov/grant/blob/master/examples/oauth-proxy | ||
[openid-connect-example]: https://github.com/simov/grant/blob/master/examples/openid-connect | ||
[migration]: https://github.com/simov/grant/blob/master/MIGRATION.md |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
16
1
116852
20
2052
995
+ Addedoauth-sign@0.9.0(transitive)
+ Addedrequest-compose@2.1.7(transitive)
+ Addedrequest-oauth@1.0.1(transitive)
+ Addeduuid@8.3.2(transitive)
- Removedoauth-sign@0.8.2(transitive)
- Removedrequest-compose@1.2.3(transitive)
- Removedrequest-oauth@0.0.3(transitive)
- Removeduuid@3.4.0(transitive)
Updatedqs@^6.9.3
Updatedrequest-compose@^2.0.0
Updatedrequest-oauth@^1.0.0