diff --git a/API.md b/API.md new file mode 100644 index 00000000..1100f1f5 --- /dev/null +++ b/API.md @@ -0,0 +1,108 @@ +# API + +Node.js client library for [OAuth2](http://oauth.net/2/). OAuth2 allows users to grant access to restricted resources by third party applications, giving them the possibility to enable and disable those accesses whenever they want. + +## .create(options) => Module + +Simple OAuth2 accepts an object with the following params. + +* `client` - required object with the following properties: + * `id` - Service registered client id. When required by the [spec](https://tools.ietf.org/html/rfc6749#appendix-B) this value will be automatically encoded. Required + * `secret` - Service registered client secret. When required by the [spec](https://tools.ietf.org/html/rfc6749#appendix-B) this value will be automatically encoded. Required + * `idParamName` - Parameter name used to send the client id. Default to **client_id** + * `secretParamName` - Parameter name used to send the client secret. Default to **client_secret** + +* `auth` - required object with the following properties: + * `tokenHost` - URL used to obtain access tokens. Required + * `tokenPath` - URL path to obtain access tokens. Default to **/oauth/token** + * `revokePath` - URL path to revoke access tokens. Default to **/oauth/revoke** + * `authorizeHost` - URL used to request an *authorization code*. Default to the value set on `auth.tokenHost` + * `authorizePath` - URL path to request an *authorization code*. Default to **/oauth/authorize** + +* `http` optional object used to set default options to the internal http library ([wreck](https://github.com/hapijs/wreck)). All options except **baseUrl** are allowed + * `json`: JSON response parsing mode. Defaults to **strict** + * `redirects` Number or redirects to follow. Defaults to **20** + * `headers` Http headers + * `accept` Acceptable http response content type. Defaults to **application/json** + * `authorization` Always overriden by the library to properly send the required credentials on each scenario + +* `options` additional options to setup how the module perform requests + * `bodyFormat` - Request's body data format. Valid options are `form` or `json`. Defaults to **form** + * `authorizationMethod` - Method used to send the *client.id*/*client.secret* authorization params at the token request. Valid options are `header` or `body`. If set to **body**, the **bodyFormat** option will be used to format the credentials. Defaults to **header** + +## Module +### .authorizationCode +This submodule provides supports for the OAuth2 [Authorization Code Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1) to support applications asking for user's resources without handling the user credentials. + +#### .authorizeURL([authorizeOptions]) => String +Creates the authorization URL from the *client configuration* and the *authorize options*. The following are supported authorize options: + +* `redirectURI` String representing the registered application URI where the user is redirected after authentication +* `scope` String or array of strings representing the application privileges +* `state` String representing an opaque value used by the client to main the state between the request and the callback + +Additional options will be automatically serialized as query params in the resulting URL. + +#### .getToken(params, [httpOptions]) => Promise +Get a new access token using the current grant type. + +* `params` + * `code` Authorization code received by the callback URL + * `redirectURI` Application callback URL + * `[scope]` Optional string or array including a subset of the original client scopes to request + +Additional options will be automatically serialized as params for the token request. + +* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options. + +### .ownerPassword +This submodule provides support for the OAuth2 [Password Owner](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.3) to support applications handling the user credentials. + +#### .getToken(params, [httpOptions]) => Promise +Get a new access token using the current grant type. + +* `params` + * `username` User identifier + * `password` User password + * `[scope]` Optional string or array including a subset of the original client scopes to request + +Additional options will be automatically serialized as params for the token request. + +* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options. + +### .clientCredentials +This submodule provides support for the OAuth2 [Client Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4) to support clients that can request access tokens using only its client credentials. + +#### .getToken(params, [httpOptions]) => Promise +Get a new access token using the current grant type. + +* `params` + * `[scope]` Optional string or array including a subset of the original client scopes to request + +Additional options will be automatically serialized as params for the token request. + +* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options. + +### .accessToken +This submodule allows for the token level operations. + +#### .create(token) => AccessToken +An access token (plain object) can be used to create a new token object with the following methods + +### AccessToken +#### .expired() => Boolean +Determines if the current access token is definitely expired or not + +#### .refresh(params) => Promise +Refreshes the current access token. The following params are allowed: + +* `params` + * `[scope]` Optional string or array including a subset of the original token scopes to request + +Additional options will be automatically serialized as query params for the token request. + +#### .revoke(tokenType) => Promise +Revokes either the access or refresh token depending on the {tokenType} value. Token type can be one of: `access_token` or `refresh_token`. + +#### .revokeAll() => Promise +Revokes both the current access and refresh tokens diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 01040ba2..7810cb71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Fork the repo on github and send a pull requests with topic branches to the ```develop``` branch. Do not forget to provide specs to your contribution. +Fork the repo on github and send a pull requests with feature branches to the ```develop``` branch. Do not forget to provide tests to your contribution. ## Repository diff --git a/README.md b/README.md index 95b985af..a5159f40 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,7 @@ [![Build Status](https://img.shields.io/travis/lelylan/simple-oauth2.svg?style=flat-square)](https://travis-ci.org/lelylan/simple-oauth2) [![Dependency Status](https://img.shields.io/david/lelylan/simple-oauth2.svg?style=flat-square)](https://david-dm.org/lelylan/simple-oauth2) -Node.js client library for [OAuth2](http://oauth.net/2/). - -OAuth2 lets users grant the access to the desired resources to third party applications, -giving them the possibility to enable and disable those accesses whenever they want. - -Simple OAuth2 supports the following flows. - -* [Authorization Code Flow](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1) (for apps with servers that can store persistent information). -* [Password Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.3) (when previous flow can't be used or during development). -* [Client Credentials Flow](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4) (the client can request an access token using only its client credentials) - -#### Thanks to Open Source - -Simple OAuth 2.0 come to life thanks to the work I've made in Lelylan, an open source microservices architecture for the Internet of Things. If this project helped you in any way, think about giving us a star on Github. - - - +Node.js client library for [OAuth2](http://oauth.net/2/). OAuth2 allows users to grant access to restricted resources by third party applications. ## Table of Contents @@ -29,25 +13,24 @@ Simple OAuth 2.0 come to life thanks to the work I've made in Lelylan, an open s - [Simple OAuth2](#simple-oauth2) - - [Thanks to Open Source](#thanks-to-open-source) - [Table of Contents](#table-of-contents) - [Requirements](#requirements) - - [Getting started](#getting-started) - - [Installation](#installation) - - [Options](#options) - - [Example of Usage](#example-of-usage) - - [OAuth2 Supported flows](#oauth2-supported-flows) - - [Authorization Code flow](#authorization-code-flow) - - [Password Credentials Flow](#password-credentials-flow) - - [Client Credentials Flow](#client-credentials-flow) - - [Helpers](#helpers) + - [Usage](#usage) + - [OAuth2 Supported grants](#oauth2-supported-grants) + - [Authorization Code](#authorization-code) + - [Password Credentials Flow](#password-credentials-flow) + - [Client Credentials Flow](#client-credentials-flow) - [Access Token object](#access-token-object) - [Errors](#errors) + - [Debugging the module](#debugging-the-module) + - [API](#api) + - [Usage examples](#usage-examples) - [Contributing](#contributing) - [Authors](#authors) - [Contributors](#contributors) - [Changelog](#changelog) - [License](#license) + - [Thanks to Open Source](#thanks-to-open-source) @@ -55,9 +38,7 @@ Simple OAuth 2.0 come to life thanks to the work I've made in Lelylan, an open s The node client library is tested against Node 8 LTS and newer versions. Older node versions are unsupported. -## Getting started - -### Installation +## Usage Install the client library using [npm](http://npmjs.org/): @@ -65,32 +46,9 @@ Install the client library using [npm](http://npmjs.org/): npm install --save simple-oauth2 ``` -### Options - -Simple OAuth2 accepts an object with the following valid params. - -* `client` - required object with the following properties: - * `id` - Service registered client id. When required by the [spec](https://tools.ietf.org/html/rfc6749#appendix-B) this value will be automatically encoded. Required. - * `secret` - Service registered client secret. When required by the [spec](https://tools.ietf.org/html/rfc6749#appendix-B) this value will be automatically encoded. Required. - * `secretParamName` - Parameter name used to send the client secret. Default to **client_secret**. - * `idParamName` - Parameter name used to send the client id. Default to **client_id**. - -* `auth` - required object with the following properties. - * `tokenHost` - String used to set the host to request the tokens to. Required. - * `tokenPath` - String path to request an access token. Default to **/oauth/token**. - * `revokePath` - String path to revoke an access token. Default to **/oauth/revoke**. - * `authorizeHost` - String used to set the host to request an "authorization code". Default to the value set on `auth.tokenHost`. - * `authorizePath` - String path to request an authorization code. Default to **/oauth/authorize**. - -* `http` optional object used to set global options to the internal http library ([wreck](https://github.com/hapijs/wreck)). - * All options except **baseUrl** are allowed. `headers.authorization` will always be overriden by the library to properly send the required credentials on each scenario. Default to `headers.Accept = application/json`. - -* `options` optional object to setup the module. - * `bodyFormat` - Format of data sent in the request body. Valid options are `form` or `json`. Defaults to **form**. - * `authorizationMethod` - Indicates the method used to send the client.id/client.secret authorization params at the token request. Valid options are `header` or `body`. If set to **body**, the **bodyFormat** option will be used to format the credentials. Defaults to **header**. +Create a new instance by specifying the minimal configuration ```javascript -// Set the configuration settings const credentials = { client: { id: '', @@ -101,195 +59,187 @@ const credentials = { } }; -// Initialize the OAuth2 Library const oauth2 = require('simple-oauth2').create(credentials); ``` -### Example of Usage - -See the [example folder](./example). +### OAuth2 Supported grants -## OAuth2 Supported flows +Depending on your use case, any of the following supported grant types may be useful: -### Authorization Code flow +#### Authorization Code -The Authorization Code flow is made up from two parts. At first your application asks to -the user the permission to access their data. If the user approves the OAuth2 server sends -to the client an authorization code. In the second part, the client POST the authorization code -along with its client secret to the oauth server in order to get the access token. +The [Authorization Code](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1) grant type is made up from two parts. At first your application asks to the user the permission to access their data. If the user approves the OAuth2 server sends to the client an authorization code. In the second part, the client POST the authorization code along with its client secret to the oauth server in order to get the access token. ```javascript -const oauth2 = require('simple-oauth2').create(credentials); +async function run() { + const oauth2 = require('simple-oauth2').create(credentials); -// Authorization oauth2 URI -const authorizationUri = oauth2.authorizationCode.authorizeURL({ - redirect_uri: 'http://localhost:3000/callback', - scope: '', // also can be an array of multiple scopes, ex. [', '', '...'] - state: '' -}); - -// Redirect example using Express (see http://expressjs.com/api.html#res.redirect) -res.redirect(authorizationUri); - -// Get the access token object (the authorization code is given from the previous step). -const tokenConfig = { - code: '', - redirect_uri: 'http://localhost:3000/callback', - scope: '', // also can be an array of multiple scopes, ex. [', '', '...'] -}; + const authorizationUri = oauth2.authorizationCode.authorizeURL({ + redirect_uri: 'http://localhost:3000/callback', + scope: '', + state: '' + }); -// Optional per-call http options -const httpOptions = {}; + // Redirect example using Express (see http://expressjs.com/api.html#res.redirect) + res.redirect(authorizationUri); -// Save the access token -try { - const result = await oauth2.authorizationCode.getToken(tokenConfig, httpOptions); - const accessToken = oauth2.accessToken.create(result); -} catch (error) { - console.log('Access Token Error', error.message); + const tokenConfig = { + code: '', + redirect_uri: 'http://localhost:3000/callback', + scope: '', + }; + + try { + const result = await oauth2.authorizationCode.getToken(tokenConfig); + const accessToken = oauth2.accessToken.create(result); + } catch (error) { + console.log('Access Token Error', error.message); + } } +run(); ``` -### Password Credentials Flow +#### Password Credentials Flow -This flow is suitable when the resource owner has a trust relationship with the -client, such as its computer operating system or a highly privileged application. -Use this flow only when other flows are not viable or when you need a fast way to -test your application. +The [Password Owner](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.3) grant type is suitable when the resource owner has a trust relationship with the client, such as its computer operating system or a highly privileged application. Use this flow only when other flows are not viable or when you need a fast way to test your application. ```javascript -const oauth2 = require('simple-oauth2').create(credentials); +async function run() { + const oauth2 = require('simple-oauth2').create(credentials); -// Get the access token object. -const tokenConfig = { - username: 'username', - password: 'password', - scope: '', // also can be an array of multiple scopes, ex. [', '', '...'] -}; - -// Optional per-call http options -const httpOptions = {}; + const tokenConfig = { + username: 'username', + password: 'password', + scope: '', + }; -// Save the access token -try { - const result = await oauth2.ownerPassword.getToken(tokenConfig, httpOptions); - const accessToken = oauth2.accessToken.create(result); -} catch (error) { - console.log('Access Token Error', error.message); + try { + const result = await oauth2.ownerPassword.getToken(tokenConfig); + const accessToken = oauth2.accessToken.create(result); + } catch (error) { + console.log('Access Token Error', error.message); + } } + +run(); ``` -### Client Credentials Flow +#### Client Credentials Flow -This flow is suitable when client is requesting access to the protected resources under its control. +The [Client Credentials](http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4) grant type is suitable when client is requesting access to the protected resources under its control. ```javascript -const oauth2 = require('simple-oauth2').create(credentials); -const tokenConfig = { - scope: '', // also can be an array of multiple scopes, ex. [', '', '...'] -}; +async function run() { + const oauth2 = require('simple-oauth2').create(credentials); -// Optional per-call http options -const httpOptions = {}; + const tokenConfig = { + scope: '', + }; -// Get the access token object for the client -try { - const result = await oauth2.clientCredentials.getToken(tokenConfig, httpOptions); - const accessToken = oauth2.accessToken.create(result); -} catch (error) { - console.log('Access Token error', error.message); + try { + const result = await oauth2.clientCredentials.getToken(tokenConfig); + const accessToken = oauth2.accessToken.create(result); + } catch (error) { + console.log('Access Token error', error.message); + } } -``` -## Helpers +run(); +``` ### Access Token object -When a token expires we need to refresh it. Simple OAuth2 offers the -AccessToken class that add a couple of useful methods to refresh the -access token when it is expired. +When a token expires we need to refresh it. Simple OAuth2 offers the AccessToken class that add a couple of useful methods to refresh the access token when it is expired. ```javascript -// Sample of a JSON access token (you got it through previous steps) -const tokenObject = { - 'access_token': '', - 'refresh_token': '', - 'expires_in': '7200' -}; +async function run() { + const tokenObject = { + 'access_token': '', + 'refresh_token': '', + 'expires_in': '7200' + }; + + let accessToken = oauth2.accessToken.create(tokenObject); + + if (accessToken.expired()) { + try { + const params = { + scope: '', + }; + + accessToken = await accessToken.refresh(params); + } catch (error) { + console.log('Error refreshing access token: ', error.message); + } + } +} -// Create the access token wrapper -let accessToken = oauth2.accessToken.create(tokenObject); +run(); +``` -// Check if the token is expired. If expired it is refreshed. -if (accessToken.expired()) { - try { - const params = { - scope: '', // also can be an array of multiple scopes, ex. [', '', '...'] - }; +The `expired` helper is useful for knowing when a token has definitively expired. However, there is a common race condition when tokens are near expiring. If an OAuth 2.0 token is issued with a `expires_in` property (as opposed to an `expires_at` property), there can be discrepancies between the time the OAuth 2.0 server issues the access token and when it is received. - accessToken = await accessToken.refresh(params); - } catch (error) { - console.log('Error refreshing access token: ', error.message); +These come down to factors such as network and processing latency and can be worked around by preemptively refreshing the access token: + +```javascript +async function run() { + // Provide a window of time before the actual expiration to refresh the token + const EXPIRATION_WINDOW_IN_SECONDS = 300; + + const { token } = accessToken; + const expirationTimeInSeconds = token.expires_at.getTime() / 1000; + const expirationWindowStart = expirationTimeInSeconds - EXPIRATION_WINDOW_IN_SECONDS; + + // If the start of the window has passed, refresh the token + const nowInSeconds = (new Date()).getTime() / 1000; + const shouldRefresh = nowInSeconds >= expirationWindowStart; + if (shouldRefresh) { + try { + accessToken = await accessToken.refresh(); + } catch (error) { + console.log('Error refreshing access token: ', error.message); + } } } + +run(); ``` -The `expired` helper is useful for knowing when a token has definitively -expired. However, there is a common race condition when tokens are near -expiring. If an OAuth 2.0 token is issued with a `expires_in` property (as -opposed to an `expires_at` property), there can be discrepancies between the -time the OAuth 2.0 server issues the access token and when it is received. -These come down to factors such as network and processing latency. This can be -worked around by preemptively refreshing the access token: +When you've done with the token or you want to log out, you can revoke the access and refresh tokens. ```javascript -// Provide a window of time before the actual expiration to refresh the token -const EXPIRATION_WINDOW_IN_SECONDS = 300; - -const { token } = accessToken; -const expirationTimeInSeconds = token.expires_at.getTime() / 1000; -const expirationWindowStart = expirationTimeInSeconds - EXPIRATION_WINDOW_IN_SECONDS; - -// If the start of the window has passed, refresh the token -const nowInSeconds = (new Date()).getTime() / 1000; -const shouldRefresh = nowInSeconds >= expirationWindowStart; -if (shouldRefresh) { +async function run() { + // Revoke both access and refresh tokens try { - accessToken = await accessToken.refresh(); + // Revoke only the access token + await accessToken.revoke('access_token'); + + // Session ended. But the refresh_token is still valid. + // Revoke the refresh token + await accessToken.revoke('refresh_token'); } catch (error) { - console.log('Error refreshing access token: ', error.message); + console.log('Error revoking token: ', error.message); } } -``` - -When you've done with the token or you want to log out, you can -revoke the access token and refresh token. -```javascript -// Revoke both access and refresh tokens -try { - // Revoke only the access token - await accessToken.revoke('access_token'); - - // Session ended. But the refresh_token is still valid. - // Revoke the refresh token - await accessToken.revoke('refresh_token'); -} catch (error) { - console.log('Error revoking token: ', error.message); -} +run(); ``` As a convenience method, you can also revoke both tokens in a single call: ```javascript -// Revoke both access and refresh tokens -try { - // Revokes both tokens, refresh token is only revoked if the access_token is properly revoked - await accessToken.revokeAll(); -} catch (error) { - console.log('Error revoking token: ', error.message); +async function run() { + // Revoke both access and refresh tokens + try { + // Revokes both tokens, refresh token is only revoked if the access_token is properly revoked + await accessToken.revokeAll(); + } catch (error) { + console.log('Error revoking token: ', error.message); + } } + +run(); ``` ### Errors @@ -301,13 +251,15 @@ Errors are returned when a 4xx or 5xx status code is received. As a standard [boom](https://github.com/hapijs/boom) error you can access any of the boom error properties. The total amount of information varies according to the generated status code. ```javascript - -try { - await oauth2.authorizationCode.getToken(); -} catch(error) { - console.log(error); +async function run() { + try { + await oauth2.authorizationCode.getToken(); + } catch(error) { + console.log(error); + } } +run(); // => { // "statusCode": 401, // "error": "Unauthorized", @@ -315,9 +267,23 @@ try { // } ``` +## Debugging the module +This module uses the [debug](https://github.com/visionmedia/debug) module to help on error diagnosis. Use the following environment variable to help in your debug journey: + +``` +DEBUG=*simple-oauth2* +``` + +## API +For a complete reference, see the module [API](./API.md). + +## Usage examples + +For complete reference examples, see the [example folder](./example). + ## Contributing -See [CONTRIBUTING](https://github.com/lelylan/simple-oauth2/blob/master/CONTRIBUTING.md) +See [CONTRIBUTING](./CONTRIBUTING.md) ## Authors @@ -327,12 +293,19 @@ See [CONTRIBUTING](https://github.com/lelylan/simple-oauth2/blob/master/CONTRIBU Special thanks to the following people for submitting patches. -* [Jonathan Samines](http://twitter.com/jonathansamines) +* [Jonathan Samines](https://github.com/jonathansamines) ## Changelog -See [CHANGELOG](https://github.com/lelylan/simple-oauth2/blob/master/CHANGELOG.md) +See [CHANGELOG](./CHANGELOG.md) ## License Simple OAuth 2.0 is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + +## Thanks to Open Source + +Simple OAuth 2.0 come to life thanks to the work I've made in Lelylan, an open source microservices architecture for the Internet of Things. If this project helped you in any way, think about giving us a star on Github. + + + diff --git a/lib/access-token.js b/lib/access-token.js index 2340bebc..d9362073 100644 --- a/lib/access-token.js +++ b/lib/access-token.js @@ -52,7 +52,7 @@ module.exports = class AccessToken { } /** - * Check if the current access token is definitely expired or not + * Determines if the current access token is definitely expired or not * * @returns {Boolean} */ @@ -63,8 +63,8 @@ module.exports = class AccessToken { /** * Refreshes the current access token * - * @param {Object} params An optional argument for additional API request params. - * @param {String|Array} params.scope A String or array of strings representing the application privileges + * @param {Object} params Optional argument for additional API request params. + * @param {String|Array} [params.scope] String or array of strings representing the application privileges * @returns {Promise} */ async refresh(params = {}) { @@ -99,7 +99,7 @@ module.exports = class AccessToken { } /** - * Revokes the current access and refresh tokens + * Revokes both the current access and refresh tokens * @returns {Promise} */ async revokeAll() { diff --git a/lib/grants/authorization-code.js b/lib/grants/authorization-code.js index 9a779886..043dbdb1 100644 --- a/lib/grants/authorization-code.js +++ b/lib/grants/authorization-code.js @@ -14,9 +14,9 @@ module.exports = class AuthorizationCode { * Get a valid redirect URL used to redirect users to an authorization page * * @param {Object} params - * @param {String} params.redirectURI A string representing the registered application URI where the user is redirected after authentication - * @param {String|Array} params.scope A String or array of strings representing the application privileges - * @param {String} params.state A String representing an option opaque value used by the client to main the state between the request and the callback + * @param {String} params.redirectURI String representing the registered application URI where the user is redirected after authentication + * @param {String|Array} params.scope String or array of strings representing the application privileges + * @param {String} params.state String representing an opaque value used by the client to main the state between the request and the callback * * @return {String} the absolute authorization url */ @@ -36,8 +36,8 @@ module.exports = class AuthorizationCode { * Requests and returns an access token from the authorization server * * @param {String} params.code Authorization code (from previous step) - * @param {String} params.redirecURI A string representing the registered application URI where the user is redirected after authentication - * @param {String|Array} [params.scope] A String or array of strings representing the application privileges + * @param {String} params.redirecURI String representing the registered application URI where the user is redirected after authentication + * @param {String|Array} [params.scope] String or array of strings representing the application privileges * @param {Object} [httpOptions] Optional http options passed through the underlying http library * @return {Promise} */