From d9e1c97b82fcd716b713c6f5d9e0d81db6e44906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Pollak?= Date: Thu, 5 Dec 2024 11:14:17 -0300 Subject: [PATCH] ESM/import version --- index.js | 7 +- lib/client.js | 28 +- lib/entities.js | 45 ++- lib/explain.js | 10 +- lib/fetch-polyfill.js | 20 + lib/helpers.js | 20 +- lib/request.js | 23 +- lib/strategies/authorized.js | 8 +- lib/strategies/bearer.js | 3 +- lib/strategies/credentials.js | 7 +- lib/strategies/index.js | 13 +- lib/strategies/oauth2.js | 726 ++++++++++++++++++++++++++++++++++ 12 files changed, 849 insertions(+), 61 deletions(-) create mode 100644 lib/fetch-polyfill.js create mode 100644 lib/strategies/oauth2.js diff --git a/index.js b/index.js index 6533709..75b2b10 100644 --- a/index.js +++ b/index.js @@ -1 +1,6 @@ -module.exports = require('./lib/client') \ No newline at end of file +// module.exports = require('./lib/client') +// const client = require('./lib/client') +// export { client } + +import { auth } from './lib/client' +export default auth; \ No newline at end of file diff --git a/lib/client.js b/lib/client.js index 4ba241e..5e95aac 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,10 +1,11 @@ -const debug = require('debug')('bootic'), - colour = require('colour'), - Request = require('./request'), - Strategies = require('./strategies'), - Entities = require('./entities'); +// import { colour } from 'colour'; +import { sendRequest, streamData } from './request'; +import { Strategies } from './strategies'; +import { root, linkedProxy, embeddedProxy } from './entities'; -const ROOT_URL = 'https://api.bootic.net/v1'; +const debug = function(str) { } + +const ROOT_URL = 'https://api.bootic.net/v1'; function Client(strategy, opts) { var Strategy = Strategies[strategy] @@ -12,6 +13,8 @@ function Client(strategy, opts) { throw new Error('Invalid strategy, valid ones are: ' + Object.keys(Strategies)); this.strategy = new Strategy(opts); + console.log('initializing client', strategy, opts) + this.rootUrl = opts.rootUrl || ROOT_URL; } @@ -33,7 +36,7 @@ Client.prototype.authorize = function() { // if (!data._class || ~data._class.indexOf('errors')) // throw new Error(data.message) - client.root = Entities.root(client, data); + client.root = root(client, data); return client.root; }) }) @@ -47,7 +50,7 @@ Client.prototype.request = function(link, params) { debug(link); console.log((link.method || 'GET').toUpperCase().blue, link.href.split('{?')[0].grey) - return Request.send(link.method, link.href, params, headers).then(function(resp) { + return sendRequest(link.method, link.href, params, headers).then(function(resp) { var code = resp.status, time = Date.now() - start; @@ -73,15 +76,16 @@ Client.prototype.request = function(link, params) { Client.prototype.stream = function(url, headers, onData, onClose) { console.log('GET'.blue, url.split('{?')[0].grey) - return Request.stream(url, headers, onData, onClose); + return streamData(url, headers, onData, onClose); } -module.exports = Client +// module.exports = Client +export default Client; -module.exports.auth = function(strategy, opts) { +export function auth(strategy, opts) { if (typeof strategy == 'object') { opts = strategy - strategy = opts.strategy || (opts.token && opts.clientId ? 'authorized' : opts.token ? 'bearer' : 'credentials') + strategy = opts.strategy || (opts.token && opts.clientId ? 'authorized' : opts.accessToken ? 'bearer' : 'credentials') } return new Client(strategy, opts).authorize() diff --git a/lib/entities.js b/lib/entities.js index 9597ee3..128a468 100644 --- a/lib/entities.js +++ b/lib/entities.js @@ -1,9 +1,11 @@ -const debug = require('debug')('bootic'); -const explain = require('./explain'); -const h = require('./helpers'); + +import explain from './explain' +import { isObject, isArray, isNumber, isFunction, containsVerb } from './helpers' const LINK_PREFIX = 'btc:'; +const debug = function(str) { } + // promise vs callback wrapper function invoke(fn, cb) { if (cb) { @@ -40,6 +42,9 @@ function buildPagedArray(itemName, client, result) { } Object.defineProperties(arr, { + 'toArray': { + get: function() { return arr } + }, 'totalItems': { get: function() { return result.total_items } }, @@ -204,7 +209,7 @@ Collection.prototype.resetFilters = function() { */ Collection.prototype.where = function(params) { - if (!params || !h.isObject(params)) + if (!params || !isObject(params)) throw new Error('Object expected, not ' + typeof(params)); this._params = params; @@ -233,8 +238,8 @@ Collection.prototype.all = function(cb) { } Collection.prototype.first = function(num, cb) { - cb = h.isFunction(num) ? num : cb; - num = h.isNumber(num) ? num : null; + cb = isFunction(num) ? num : cb; + num = isNumber(num) ? num : null; return invoke(function(done) { this._fetchItems(function(items) { @@ -244,8 +249,8 @@ Collection.prototype.first = function(num, cb) { } Collection.prototype.last = function(num, cb) { - cb = h.isFunction(num) ? num : cb; - num = h.isNumber(num) ? num : null; + cb = isFunction(num) ? num : cb; + num = isNumber(num) ? num : null; return invoke(function(done) { this._fetchItems(function(items) { @@ -307,8 +312,8 @@ LinkedEntity.prototype = Object.create(Entity.prototype) LinkedEntity.prototype.constructor = LinkedEntity LinkedEntity.prototype.get = function(params, cb) { - cb = h.isFunction(params) ? params : cb; - params = h.isObject(params) ? params : {}; + cb = isFunction(params) ? params : cb; + params = isObject(params) ? params : {}; return invoke(function(done) { return this._client.request(this._link, params).then(function(result) { @@ -410,13 +415,13 @@ LinkedCollection.prototype.desc = function(count) { LinkedCollection.prototype.first = function(num, cb) { this.asc(); - if (h.isNumber(num)) this.limit(num); + if (isNumber(num)) this.limit(num); return Collection.prototype.first.call(this, num, cb); } LinkedCollection.prototype.last = function(num, cb) { this.desc(); - if (h.isNumber(num)) this.limit(num); + if (isNumber(num)) this.limit(num); return Collection.prototype.last.call(this, num, cb); } @@ -522,7 +527,7 @@ VirtualCollection.prototype._fetch = function(cb) { // TODO: less duplication, if possible. function embeddedProxy(client, name, data) { - if (h.isArray(data)) { + if (isArray(data)) { var target = new Collection(name, client, data) } else { var target = new Entity(name, client, data) @@ -543,7 +548,7 @@ function detectProxy(client, data, defaultName) { } function linkedProxy(client, name, link, singular) { - if (link.type || h.containsVerb(name) || (link.method && link.method.toLowerCase() != 'get')) + if (link.type || containsVerb(name) || (link.method && link.method.toLowerCase() != 'get')) return new LinkedAction(name, client, link) if (name[name.length-1] != 's') // singular, not a collection @@ -610,7 +615,7 @@ let proxyHandler = { //////////////////////////////////////////////////////////////// // the root exports -exports.root = function(client, data) { +function root(client, data) { if (data && (data._links || {})['btc:all_shops'] && (data._embedded || {})['shops']) delete data._embedded['shops'] // so root.shops returns 'all_shops' link @@ -618,5 +623,11 @@ exports.root = function(client, data) { return new Proxy(target, proxyHandler) } -exports.embeddedProxy = embeddedProxy; -exports.linkedProxy = linkedProxy; +export { + root, + embeddedProxy, + linkedProxy +} + +// exports.embeddedProxy = embeddedProxy; +// exports.linkedProxy = linkedProxy; diff --git a/lib/explain.js b/lib/explain.js index f2d68a9..d1a5786 100644 --- a/lib/explain.js +++ b/lib/explain.js @@ -1,5 +1,7 @@ -const colour = require('colour') -const containsVerb = require('./helpers').containsVerb +// const colour = require('colour') +import colour from 'colour'; +import { containsVerb } from './helpers'; + const LINK_PREFIX = 'btc:' function explain(el) { @@ -46,4 +48,6 @@ function explain(el) { } } -module.exports = explain \ No newline at end of file +export default explain; + +// module.exports = explain \ No newline at end of file diff --git a/lib/fetch-polyfill.js b/lib/fetch-polyfill.js new file mode 100644 index 0000000..4c5fcc3 --- /dev/null +++ b/lib/fetch-polyfill.js @@ -0,0 +1,20 @@ +// fetch-polyfill.js +import fetch, { + Blob, + blobFrom, + blobFromSync, + File, + fileFrom, + fileFromSync, + FormData, + Headers, + Request, + Response, +} from 'node-fetch' + +if (!globalThis.fetch) { + globalThis.fetch = fetch + globalThis.Headers = Headers + globalThis.Request = Request + globalThis.Response = Response +} \ No newline at end of file diff --git a/lib/helpers.js b/lib/helpers.js index 1befa7c..0bc8737 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,23 +1,27 @@ -var helpers = {}; - -helpers.isObject = function(obj) { +function isObject(obj) { return obj && obj.constructor.name == 'Object' } -helpers.isArray = function(obj) { +function isArray(obj) { return obj && obj.constructor.name == 'Array' } -helpers.isNumber = function(num) { +function isNumber(num) { return typeof num == 'number' } -helpers.isFunction = function(fn) { +function isFunction(fn) { return typeof fn == 'function' } -helpers.containsVerb = function(word) { +function containsVerb(word) { return !!word.replace('btc:', '').match(/^(get|update|create|remove|destroy)_/) } -module.exports = helpers; \ No newline at end of file +export { + isObject, + isArray, + isNumber, + isFunction, + containsVerb, +} \ No newline at end of file diff --git a/lib/request.js b/lib/request.js index 544c09e..d719d3c 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,10 +1,12 @@ -var debug = require('debug')('bootic'); -var uriTemplate = require('uri-templates'); +// var debug = require('debug')('bootic'); +// var uriTemplate = require('uri-templates'); -if (typeof fetch == 'undefined') { - var fetch = require('node-fetch-polyfill'); - require('http').globalAgent.keepAlive = true; -} +const debug = function(str) { } +import uriTemplate from 'uri-templates'; + +// import { fetch } from 'node-fetch-polyfill'; +// import fetch from 'node-fetch'; +// import './fetch-polyfill' function sendRequest(method, url, params, headers) { var template, params = params || {}, options = { @@ -90,5 +92,10 @@ function streamData(url, headers, cb, done) { }); } -exports.send = sendRequest; -exports.stream = streamData; \ No newline at end of file +// exports.send = sendRequest; +// exports.stream = streamData; + +export { + sendRequest, + streamData +} \ No newline at end of file diff --git a/lib/strategies/authorized.js b/lib/strategies/authorized.js index 64c539c..10ec35c 100644 --- a/lib/strategies/authorized.js +++ b/lib/strategies/authorized.js @@ -1,6 +1,8 @@ -var ClientOAuth2 = require('client-oauth2') +// var ClientOAuth2 = require('client-oauth2') +// import ClientOAuth2 from 'client-oauth2'; +import ClientOAuth2 from './oauth2'; -function AuthorizedStrategy(opts) { +export default function AuthorizedStrategy(opts) { if (!opts.clientId || !opts.clientSecret || (!opts.token && !opts.accessToken)) throw new Error('token, clientId and clientSecret required!') @@ -23,5 +25,3 @@ function AuthorizedStrategy(opts) { return { getToken: getToken, canRefresh: true } } - -module.exports = AuthorizedStrategy \ No newline at end of file diff --git a/lib/strategies/bearer.js b/lib/strategies/bearer.js index 946cead..7251e35 100644 --- a/lib/strategies/bearer.js +++ b/lib/strategies/bearer.js @@ -1,4 +1,4 @@ -function BearerStrategy(opts) { +export default function BearerStrategy(opts) { if (!opts.accessToken && !opts.token) throw new Error('accessToken required!') @@ -13,4 +13,3 @@ function BearerStrategy(opts) { return { getToken: getToken, canRefresh: false } } -module.exports = BearerStrategy \ No newline at end of file diff --git a/lib/strategies/credentials.js b/lib/strategies/credentials.js index 092b43a..a0b137a 100644 --- a/lib/strategies/credentials.js +++ b/lib/strategies/credentials.js @@ -1,6 +1,8 @@ -var ClientOAuth2 = require('client-oauth2') +// var ClientOAuth2 = require('client-oauth2') +// import ClientOAuth2 from 'client-oauth2'; +import ClientOAuth2 from './oauth2'; -function CredentialsStrategy(opts) { +export default function CredentialsStrategy(opts) { if (!opts.clientId || !opts.clientSecret) throw new Error('clientId and clientSecret required!') @@ -21,4 +23,3 @@ function CredentialsStrategy(opts) { return { getToken: getToken, canRefresh: true } } -module.exports = CredentialsStrategy \ No newline at end of file diff --git a/lib/strategies/index.js b/lib/strategies/index.js index e626e6b..46f52bd 100644 --- a/lib/strategies/index.js +++ b/lib/strategies/index.js @@ -1,3 +1,10 @@ -exports.authorized = require('./authorized') -exports.bearer = require('./bearer') -exports.credentials = require('./credentials') \ No newline at end of file +import authorized from './authorized'; +import bearer from './bearer'; +import credentials from './credentials'; + +export const Strategies = { + authorized, + bearer, + credentials +} + diff --git a/lib/strategies/oauth2.js b/lib/strategies/oauth2.js new file mode 100644 index 0000000..f088f0d --- /dev/null +++ b/lib/strategies/oauth2.js @@ -0,0 +1,726 @@ +// import { Buffer } from 'safe-buffer'; +import { parse, stringify } from 'querystringify'; + +// var defaultRequest = require('./request') + +function defaultRequest (method, url, body, headers) { + return fetch(url, { + body: body, + method: method, + headers: headers + }).then(function (res) { + return res.text() + .then(body => { + return { + status: res.status, + body: body + } + }) + }) +} + +const DEFAULT_URL_BASE = 'https://example.org/' + +// var btoa +// if (typeof Buffer === 'function') { +// btoa = btoaBuffer +// } else { +// btoa = window.btoa.bind(window) +// } + +/** + * Export `ClientOAuth2` class. + */ +// module.exports = ClientOAuth2 + +/** + * Default headers for executing OAuth 2.0 flows. + */ +var DEFAULT_HEADERS = { + Accept: 'application/json, application/x-www-form-urlencoded', + 'Content-Type': 'application/x-www-form-urlencoded' +} + +/** + * Format error response types to regular strings for displaying to clients. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.1.2.1 + */ +var ERROR_RESPONSES = { + invalid_request: [ + 'The request is missing a required parameter, includes an', + 'invalid parameter value, includes a parameter more than', + 'once, or is otherwise malformed.' + ].join(' '), + invalid_client: [ + 'Client authentication failed (e.g., unknown client, no', + 'client authentication included, or unsupported', + 'authentication method).' + ].join(' '), + invalid_grant: [ + 'The provided authorization grant (e.g., authorization', + 'code, resource owner credentials) or refresh token is', + 'invalid, expired, revoked, does not match the redirection', + 'URI used in the authorization request, or was issued to', + 'another client.' + ].join(' '), + unauthorized_client: [ + 'The client is not authorized to request an authorization', + 'code using this method.' + ].join(' '), + unsupported_grant_type: [ + 'The authorization grant type is not supported by the', + 'authorization server.' + ].join(' '), + access_denied: [ + 'The resource owner or authorization server denied the request.' + ].join(' '), + unsupported_response_type: [ + 'The authorization server does not support obtaining', + 'an authorization code using this method.' + ].join(' '), + invalid_scope: [ + 'The requested scope is invalid, unknown, or malformed.' + ].join(' '), + server_error: [ + 'The authorization server encountered an unexpected', + 'condition that prevented it from fulfilling the request.', + '(This error code is needed because a 500 Internal Server', + 'Error HTTP status code cannot be returned to the client', + 'via an HTTP redirect.)' + ].join(' '), + temporarily_unavailable: [ + 'The authorization server is currently unable to handle', + 'the request due to a temporary overloading or maintenance', + 'of the server.' + ].join(' ') +} + +/** + * Support base64 in node like how it works in the browser. + * + * @param {string} string + * @return {string} + */ +function btoaBuffer (string) { + return btoa(string) + // return Buffer.from(string).toString('base64') +} + +/** + * Check if properties exist on an object and throw when they aren't. + * + * @throws {TypeError} If an expected property is missing. + * + * @param {Object} obj + * @param {...string} props + */ +function expects (obj) { + for (var i = 1; i < arguments.length; i++) { + var prop = arguments[i] + + if (obj[prop] == null) { + throw new TypeError('Expected "' + prop + '" to exist') + } + } +} + +/** + * Pull an authentication error from the response data. + * + * @param {Object} data + * @return {string} + */ +function getAuthError (body) { + var message = ERROR_RESPONSES[body.error] || + body.error_description || + body.error + + if (message) { + var err = new Error(message) + err.body = body + err.code = 'EAUTH' + return err + } +} + +/** + * Attempt to parse response body as JSON, fall back to parsing as a query string. + * + * @param {string} body + * @return {Object} + */ +function parseResponseBody (body) { + try { + return JSON.parse(body) + } catch (e) { + return parse(body) + } +} + +/** + * Sanitize the scopes option to be a string. + * + * @param {Array} scopes + * @return {string} + */ +function sanitizeScope (scopes) { + return Array.isArray(scopes) ? scopes.join(' ') : toString(scopes) +} + +/** + * Create a request uri based on an options object and token type. + * + * @param {Object} options + * @param {string} tokenType + * @return {string} + */ +function createUri (options, tokenType) { + // Check the required parameters are set. + expects(options, 'clientId', 'authorizationUri') + + const qs = { + client_id: options.clientId, + redirect_uri: options.redirectUri, + response_type: tokenType, + state: options.state + } + if (options.scopes !== undefined) { + qs.scope = sanitizeScope(options.scopes) + } + + const sep = options.authorizationUri.includes('?') ? '&' : '?' + return options.authorizationUri + sep + stringify( + Object.assign(qs, options.query)) +} + +/** + * Create basic auth header. + * + * @param {string} username + * @param {string} password + * @return {string} + */ +function auth (username, password) { + return 'Basic ' + btoa(toString(username) + ':' + toString(password)) +} + +/** + * Ensure a value is a string. + * + * @param {string} str + * @return {string} + */ +function toString (str) { + return str == null ? '' : String(str) +} + +/** + * Merge request options from an options object. + */ +function requestOptions (requestOptions, options) { + return { + url: requestOptions.url, + method: requestOptions.method, + body: Object.assign({}, requestOptions.body, options.body), + query: Object.assign({}, requestOptions.query, options.query), + headers: Object.assign({}, requestOptions.headers, options.headers) + } +} + +/** + * Construct an object that can handle the multiple OAuth 2.0 flows. + * + * @param {Object} options + */ +export default function ClientOAuth2 (options, request) { + this.options = options + this.request = request || defaultRequest + + this.code = new CodeFlow(this) + this.token = new TokenFlow(this) + this.owner = new OwnerFlow(this) + this.credentials = new CredentialsFlow(this) + this.jwt = new JwtBearerFlow(this) +} + +/** + * Alias the token constructor. + * + * @type {Function} + */ +ClientOAuth2.Token = ClientOAuth2Token + +/** + * Create a new token from existing data. + * + * @param {string} access + * @param {string} [refresh] + * @param {string} [type] + * @param {Object} [data] + * @return {Object} + */ +ClientOAuth2.prototype.createToken = function (access, refresh, type, data) { + var options = Object.assign( + {}, + data, + typeof access === 'string' ? { access_token: access } : access, + typeof refresh === 'string' ? { refresh_token: refresh } : refresh, + typeof type === 'string' ? { token_type: type } : type + ) + + return new ClientOAuth2.Token(this, options) +} + +/** + * Using the built-in request method, we'll automatically attempt to parse + * the response. + * + * @param {Object} options + * @return {Promise} + */ +ClientOAuth2.prototype._request = function (options) { + var url = options.url + var body = stringify(options.body) + var query = stringify(options.query) + + if (query) { + url += (url.indexOf('?') === -1 ? '?' : '&') + query + } + + return this.request(options.method, url, body, options.headers) + .then(function (res) { + var body = parseResponseBody(res.body) + var authErr = getAuthError(body) + + if (authErr) { + return Promise.reject(authErr) + } + + if (res.status < 200 || res.status >= 399) { + var statusErr = new Error('HTTP status ' + res.status) + statusErr.status = res.status + statusErr.body = res.body + statusErr.code = 'ESTATUS' + return Promise.reject(statusErr) + } + + return body + }) +} + +/** + * General purpose client token generator. + * + * @param {Object} client + * @param {Object} data + */ +function ClientOAuth2Token (client, data) { + this.client = client + this.data = data + this.tokenType = data.token_type && data.token_type.toLowerCase() + this.accessToken = data.access_token + this.refreshToken = data.refresh_token + + this.expiresIn(Number(data.expires_in)) +} + +/** + * Expire the token after some time. + * + * @param {number|Date} duration Seconds from now to expire, or a date to expire on. + * @return {Date} + */ +ClientOAuth2Token.prototype.expiresIn = function (duration) { + if (typeof duration === 'number') { + this.expires = new Date() + this.expires.setSeconds(this.expires.getSeconds() + duration) + } else if (duration instanceof Date) { + this.expires = new Date(duration.getTime()) + } else { + throw new TypeError('Unknown duration: ' + duration) + } + + return this.expires +} + +/** + * Sign a standardised request object with user authentication information. + * + * @param {Object} requestObject + * @return {Object} + */ +ClientOAuth2Token.prototype.sign = function (requestObject) { + if (!this.accessToken) { + throw new Error('Unable to sign without access token') + } + + requestObject.headers = requestObject.headers || {} + + if (this.tokenType === 'bearer') { + requestObject.headers.Authorization = 'Bearer ' + this.accessToken + } else { + var parts = requestObject.url.split('#') + var token = 'access_token=' + this.accessToken + var url = parts[0].replace(/[?&]access_token=[^&#]/, '') + var fragment = parts[1] ? '#' + parts[1] : '' + + // Prepend the correct query string parameter to the url. + requestObject.url = url + (url.indexOf('?') > -1 ? '&' : '?') + token + fragment + + // Attempt to avoid storing the url in proxies, since the access token + // is exposed in the query parameters. + requestObject.headers.Pragma = 'no-store' + requestObject.headers['Cache-Control'] = 'no-store' + } + + return requestObject +} + +/** + * Refresh a user access token with the supplied token. + * + * @param {Object} opts + * @return {Promise} + */ +ClientOAuth2Token.prototype.refresh = function (opts) { + var self = this + var options = Object.assign({}, this.client.options, opts) + + if (!this.refreshToken) { + return Promise.reject(new Error('No refresh token')) + } + + return this.client._request(requestOptions({ + url: options.accessTokenUri, + method: 'POST', + headers: Object.assign({}, DEFAULT_HEADERS, { + Authorization: auth(options.clientId, options.clientSecret) + }), + body: { + refresh_token: this.refreshToken, + grant_type: 'refresh_token' + } + }, options)) + .then(function (data) { + return self.client.createToken(Object.assign({}, self.data, data)) + }) +} + +/** + * Check whether the token has expired. + * + * @return {boolean} + */ +ClientOAuth2Token.prototype.expired = function () { + return Date.now() > this.expires.getTime() +} + +/** + * Support resource owner password credentials OAuth 2.0 grant. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.3 + * + * @param {ClientOAuth2} client + */ +function OwnerFlow (client) { + this.client = client +} + +/** + * Make a request on behalf of the user credentials to get an access token. + * + * @param {string} username + * @param {string} password + * @param {Object} [opts] + * @return {Promise} + */ +OwnerFlow.prototype.getToken = function (username, password, opts) { + var self = this + var options = Object.assign({}, this.client.options, opts) + + const body = { + username: username, + password: password, + grant_type: 'password' + } + if (options.scopes !== undefined) { + body.scope = sanitizeScope(options.scopes) + } + + return this.client._request(requestOptions({ + url: options.accessTokenUri, + method: 'POST', + headers: Object.assign({}, DEFAULT_HEADERS, { + Authorization: auth(options.clientId, options.clientSecret) + }), + body: body + }, options)) + .then(function (data) { + return self.client.createToken(data) + }) +} + +/** + * Support implicit OAuth 2.0 grant. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.2 + * + * @param {ClientOAuth2} client + */ +function TokenFlow (client) { + this.client = client +} + +/** + * Get the uri to redirect the user to for implicit authentication. + * + * @param {Object} [opts] + * @return {string} + */ +TokenFlow.prototype.getUri = function (opts) { + var options = Object.assign({}, this.client.options, opts) + + return createUri(options, 'token') +} + +/** + * Get the user access token from the uri. + * + * @param {string|Object} uri + * @param {Object} [opts] + * @return {Promise} + */ +TokenFlow.prototype.getToken = function (uri, opts) { + var options = Object.assign({}, this.client.options, opts) + var url = typeof uri === 'object' ? uri : new URL(uri, DEFAULT_URL_BASE) + var expectedUrl = new URL(options.redirectUri, DEFAULT_URL_BASE) + + if (typeof url.pathname === 'string' && url.pathname !== expectedUrl.pathname) { + return Promise.reject( + new TypeError('Redirected path should match configured path, but got: ' + url.pathname) + ) + } + + // If no query string or fragment exists, we won't be able to parse + // any useful information from the uri. + if (!url.hash && !url.search) { + return Promise.reject(new TypeError('Unable to process uri: ' + uri)) + } + + // Extract data from both the fragment and query string. The fragment is most + // important, but the query string is also used because some OAuth 2.0 + // implementations (Instagram) have a bug where state is passed via query. + var data = Object.assign( + {}, + typeof url.search === 'string' ? parse(url.search.substr(1)) : (url.search || {}), + typeof url.hash === 'string' ? parse(url.hash.substr(1)) : (url.hash || {}) + ) + + var err = getAuthError(data) + + // Check if the query string was populated with a known error. + if (err) { + return Promise.reject(err) + } + + // Check whether the state matches. + if (options.state != null && data.state !== options.state) { + return Promise.reject(new TypeError('Invalid state: ' + data.state)) + } + + // Initalize a new token and return. + return Promise.resolve(this.client.createToken(data)) +} + +/** + * Support client credentials OAuth 2.0 grant. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.4 + * + * @param {ClientOAuth2} client + */ +function CredentialsFlow (client) { + this.client = client +} + +/** + * Request an access token using the client credentials. + * + * @param {Object} [opts] + * @return {Promise} + */ +CredentialsFlow.prototype.getToken = function (opts) { + var self = this + var options = Object.assign({}, this.client.options, opts) + + expects(options, 'clientId', 'clientSecret', 'accessTokenUri') + + const body = { + grant_type: 'client_credentials' + } + + if (options.scopes !== undefined) { + body.scope = sanitizeScope(options.scopes) + } + + return this.client._request(requestOptions({ + url: options.accessTokenUri, + method: 'POST', + headers: Object.assign({}, DEFAULT_HEADERS, { + Authorization: auth(options.clientId, options.clientSecret) + }), + body: body + }, options)) + .then(function (data) { + return self.client.createToken(data) + }) +} + +/** + * Support authorization code OAuth 2.0 grant. + * + * Reference: http://tools.ietf.org/html/rfc6749#section-4.1 + * + * @param {ClientOAuth2} client + */ +function CodeFlow (client) { + this.client = client +} + +/** + * Generate the uri for doing the first redirect. + * + * @param {Object} [opts] + * @return {string} + */ +CodeFlow.prototype.getUri = function (opts) { + var options = Object.assign({}, this.client.options, opts) + + return createUri(options, 'code') +} + +/** + * Get the code token from the redirected uri and make another request for + * the user access token. + * + * @param {string|Object} uri + * @param {Object} [opts] + * @return {Promise} + */ +CodeFlow.prototype.getToken = function (uri, opts) { + var self = this + var options = Object.assign({}, this.client.options, opts) + + expects(options, 'clientId', 'accessTokenUri') + + var url = typeof uri === 'object' ? uri : new URL(uri, DEFAULT_URL_BASE) + + if ( + typeof options.redirectUri === 'string' && + typeof url.pathname === 'string' && + url.pathname !== (new URL(options.redirectUri, DEFAULT_URL_BASE)).pathname + ) { + return Promise.reject( + new TypeError('Redirected path should match configured path, but got: ' + url.pathname) + ) + } + + if (!url.search || !url.search.substr(1)) { + return Promise.reject(new TypeError('Unable to process uri: ' + uri)) + } + + var data = typeof url.search === 'string' + ? parse(url.search.substr(1)) + : (url.search || {}) + var err = getAuthError(data) + + if (err) { + return Promise.reject(err) + } + + if (options.state != null && data.state !== options.state) { + return Promise.reject(new TypeError('Invalid state: ' + data.state)) + } + + // Check whether the response code is set. + if (!data.code) { + return Promise.reject(new TypeError('Missing code, unable to request token')) + } + + var headers = Object.assign({}, DEFAULT_HEADERS) + var body = { code: data.code, grant_type: 'authorization_code', redirect_uri: options.redirectUri } + + // `client_id`: REQUIRED, if the client is not authenticating with the + // authorization server as described in Section 3.2.1. + // Reference: https://tools.ietf.org/html/rfc6749#section-3.2.1 + if (options.clientSecret) { + headers.Authorization = auth(options.clientId, options.clientSecret) + } else { + body.client_id = options.clientId + } + + return this.client._request(requestOptions({ + url: options.accessTokenUri, + method: 'POST', + headers: headers, + body: body + }, options)) + .then(function (data) { + return self.client.createToken(data) + }) +} + +/** + * Support JSON Web Token (JWT) Bearer Token OAuth 2.0 grant. + * + * Reference: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-2.1 + * + * @param {ClientOAuth2} client + */ +function JwtBearerFlow (client) { + this.client = client +} + +/** + * Request an access token using a JWT token. + * + * @param {string} token A JWT token. + * @param {Object} [opts] + * @return {Promise} + */ +JwtBearerFlow.prototype.getToken = function (token, opts) { + var self = this + var options = Object.assign({}, this.client.options, opts) + var headers = Object.assign({}, DEFAULT_HEADERS) + + expects(options, 'accessTokenUri') + + // Authentication of the client is optional, as described in + // Section 3.2.1 of OAuth 2.0 [RFC6749] + if (options.clientId) { + headers.Authorization = auth(options.clientId, options.clientSecret) + } + + const body = { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: token + } + + if (options.scopes !== undefined) { + body.scope = sanitizeScope(options.scopes) + } + + return this.client._request(requestOptions({ + url: options.accessTokenUri, + method: 'POST', + headers: headers, + body: body + }, options)) + .then(function (data) { + return self.client.createToken(data) + }) +} + + +// export default ClientOAuth2; \ No newline at end of file