New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

fetch-mw-oauth2

Package Overview
Dependencies
Maintainers
1
Versions
36
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fetch-mw-oauth2 - npm Package Compare versions

Comparing version 1.0.0 to 2.0.0

dist/client.d.ts

2

browser/fetch-mw-oauth2.min.js

@@ -1,2 +0,2 @@

!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.fetchMwOAuth2=t():e.fetchMwOAuth2=t()}(self,(function(){return(()=>{"use strict";var e={681:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.encode=void 0,t.encode=function(e){return btoa(e)}},443:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0});class r extends Error{constructor(e,t,r){super(e),this.oauth2Code=t,this.httpCode=r}}t.default=r},13:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0});const n=r(882);t.default=class{constructor(e,t=null){if(!e.grantType&&!t&&!e.accessToken)throw new Error("If no grantType is specified, a token must be provided");this.options=e,e.accessToken&&(console.warn("[fetch-mw-oauth2] Specifying accessToken via the options argument in the constructor of OAuth2 is deprecated. Please supply the options in the second argument. Backwards compatability will be removed in a future version of this library"),t={accessToken:e.accessToken,refreshToken:e.refreshToken||null,expiresAt:null}),this.token=t,this.activeRefresh=null,this.refreshTimer=null,this.scheduleRefresh()}async fetch(e,t){const r=new Request(e,t);return this.fetchMw(r,(e=>fetch(e)))}async fetchMw(e,t){const r=await this.getAccessToken();let n=e.clone();n.headers.set("Authorization","Bearer "+r);let o=await t(n);if(!o.ok&&401===o.status){const r=await this.refreshToken();n=e.clone(),n.headers.set("Authorization","Bearer "+r.accessToken),o=await t(n)}return o}async getToken(){return this.token&&(null===this.token.expiresAt||this.token.expiresAt>Date.now())?this.token:this.refreshToken()}async getAccessToken(){return(await this.getToken()).accessToken}async refreshToken(){if(this.activeRefresh)return this.activeRefresh;this.activeRefresh=(0,n.refreshToken)(this.options,this.token);try{const e=await this.activeRefresh;return this.token=e,this.scheduleRefresh(),e}catch(e){throw this.options.onAuthError&&this.options.onAuthError(e),e}finally{this.activeRefresh=null}}scheduleRefresh(){if(this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null),!this.token||!this.token.expiresAt||!this.token.refreshToken)return;const e=this.token.expiresAt-Date.now();e<12e4||(this.refreshTimer=setTimeout((async()=>{try{await this.refreshToken()}catch(e){console.error("[fetch-mw-oauth2] error while doing a background OAuth2 auto-refresh",e)}}),e-6e4))}}},882:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.refreshToken=t.objToQueryString=void 0;const n=r(681),o=r(443);function s(e){return Object.entries(e).map((([e,t])=>void 0===t?"":encodeURIComponent(e)+"="+encodeURIComponent(t))).join("&")}t.objToQueryString=s,t.refreshToken=async function e(t,r){let i;const c=r;if(null==c?void 0:c.refreshToken)i={grant_type:"refresh_token",refresh_token:c.refreshToken},void 0===t.clientSecret&&(i.client_id=t.clientId);else switch(t.grantType){case"client_credentials":i={grant_type:"client_credentials"},t.scope&&(i.scope=t.scope.join(" "));break;case"password":i={grant_type:"password",username:t.userName,password:t.password},t.scope&&(i.scope=t.scope.join(" "));break;case"authorization_code":i={grant_type:"authorization_code",code:t.code,redirect_uri:t.redirectUri,client_id:t.clientId,code_verifier:t.codeVerifier};break;default:throw"string"==typeof t.grantType?new Error("Unknown grantType: "+t.grantType):new Error('Cannot obtain an access token if no "grantType" is specified')}const a={"Content-Type":"application/x-www-form-urlencoded"};let h=!1;if(void 0!==t.clientSecret){h=!0;const e=(0,n.encode)(t.clientId+":"+t.clientSecret);a.Authorization="Basic "+e}const u=await fetch(t.tokenEndpoint,{method:"POST",headers:a,body:s(i)}),f=await u.json();if(!u.ok){if("refresh_token"===i.grant_type&&t.grantType)return e(t,null);const r=u.status;let n,s;throw f.error?(n="OAuth2 error "+f.error+".",f.error_description&&(n+=" "+f.error_description),s=f.error):(n="HTTP Error "+u.status+" "+u.statusText,401===u.status&&h&&(n+=". It's likely that the clientId and/or clientSecret was incorrect"),s=null),new o.default(n,s,r)}const l={accessToken:f.access_token,expiresAt:f.expires_in?Date.now()+1e3*f.expires_in:null,refreshToken:f.refresh_token?f.refresh_token:null};return t.onTokenUpdate&&t.onTokenUpdate(l),l}}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.OAuth2Error=e.OAuth2=e.fetchMwOAuth2=e.default=void 0;var t=r(13);Object.defineProperty(e,"default",{enumerable:!0,get:function(){return t.default}}),Object.defineProperty(e,"fetchMwOAuth2",{enumerable:!0,get:function(){return t.default}}),Object.defineProperty(e,"OAuth2",{enumerable:!0,get:function(){return t.default}});var o=r(443);Object.defineProperty(e,"OAuth2Error",{enumerable:!0,get:function(){return o.default}})})(),n})()}));
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.fetchMwOAuth2=t():e.fetchMwOAuth2=t()}(self,(()=>(()=>{"use strict";var e={934:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.OAuth2Client=void 0;const n=r(443),s=r(618);function i(e,t){return new URL(e,t).toString()}t.OAuth2Client=class{constructor(e){this.discoveryDone=!1,this.serverMetadata=null,this.settings=e}async refreshToken(e){if(!e.refreshToken)throw new Error("This token didn't have a refreshToken. It's not possible to refresh this");const t={grant_type:"refresh_token",refresh_token:e.refreshToken};return this.settings.clientSecret||(t.client_id=this.settings.clientId),this.request("tokenEndpoint",t)}async clientCredentials(e){var t;const r={grant_type:"client_credentials",scope:null===(t=null==e?void 0:e.scope)||void 0===t?void 0:t.join(" ")};if(!this.settings.clientSecret)throw new Error("A clientSecret must be provied to use client_credentials");return this.request("tokenEndpoint",r)}async password(e){var t;const r={grant_type:"password",...e,scope:null===(t=e.scope)||void 0===t?void 0:t.join(" ")};if(!this.settings.clientSecret)throw new Error("A clientSecret must be provied to use client_credentials");return this.request("tokenEndpoint",r)}async authorizationCode(e){return new s.AuthorizationCodeClient(this,e.redirectUri,e.state)}async introspect(e){const t={token:e.accessToken,token_type_hint:"access_token"};return this.request("introspectionEndpoint",t)}async getEndpoint(e){if(void 0!==this.settings[e])return i(this.settings[e],this.settings.server);if("discoveryEndpoint"!==e&&(await this.discover(),void 0!==this.settings[e]))return i(this.settings[e],this.settings.server);if(!this.settings.server)throw new Error(`Could not determine the location of ${e}. Either specify ${e} in the settings, or the "server" endpoint to let the client discover it.`);switch(e){case"authorizationEndpoint":return i("/authorize",this.settings.server);case"tokenEndpoint":return i("/token",this.settings.server);case"discoveryEndpoint":return i("/.well-known/oauth-authorization-server",this.settings.server);case"introspectionEndpoint":return i("/introspect",this.settings.server)}}async discover(){if(this.discoveryDone)return;let e;this.discoveryDone=!0;try{e=await this.getEndpoint("discoveryEndpoint")}catch(e){return void console.warn('[oauth2] OAuth2 discovery endpoint could not be determined. Either specify the "server" or "discoveryEndpoint')}const t=await fetch(e,{headers:{Accept:"application/json"}});if(!t.ok)return;if(!t.headers.has("Content-Type")||t.headers.get("Content-Type").startsWith("application/json"))return void console.warn("[oauth2] OAuth2 discovery endpoint was not a JSON response. Response is ignored");this.serverMetadata=await t.json();const r=[["authorization_endpoint","authorizationEndpoint"],["token_endpoint","tokenEndpoint"],["introspection_endpoint","introspectionEndpoint"]];if(null!==this.serverMetadata)for(const[t,n]of r)this.serverMetadata[t]&&(this.settings[n]=i(this.serverMetadata[t],e))}async request(e,t){var r;const s=await this.getEndpoint(e),i={};if("authorization_code"!==t.grant_type&&this.settings.clientSecret){const e=btoa(this.settings.clientId+":"+this.settings.clientSecret);i.Authorization="Basic "+e}const o=await fetch(s,{method:" POST",body:new URLSearchParams(t),headers:i});if(o.ok){const e=await o.json();return{accessToken:e.access_token,expiresAt:e.expires_in?Date.now()+1e3*e.expires_in:null,refreshToken:null!==(r=e.refresh_token)&&void 0!==r?r:null}}let a,h,c;throw o.headers.has("Content-Type")&&o.headers.get("Content-Type").startsWith("application/json")&&(a=await o.json()),(null==a?void 0:a.error)?(h="OAuth2 error "+a.error+".",a.error_description&&(h+=" "+a.error_description),c=a.error):(h="HTTP Error "+o.status+" "+o.statusText,401===o.status&&this.settings.clientSecret&&(h+=". It's likely that the clientId and/or clientSecret was incorrect"),c=null),new n.OAuth2Error(h,c,o.status)}}},618:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.AuthorizationCodeClient=void 0;const n=r(443);t.AuthorizationCodeClient=class{constructor(e,t,r){this.client=e,this.redirectUri=t,this.state=r}async getAuthorizeUri(){const e={response_type:"code",client_id:this.client.settings.clientId,redirect_uri:this.redirectUri};this.state&&(e.state=this.state);const t=new URLSearchParams(e);return await this.client.getEndpoint("authorizationEndpoint")+"?"+t.toString()}async validateResponse(e){var t;const r=new URL(e).searchParams;if(r.has("error"))throw new n.OAuth2Error(null!==(t=r.get("error_description"))&&void 0!==t?t:"OAuth2 error",r.get("error"),0);if(!r.has("code"))throw new Error(`The url did not contain a code parameter ${e}`);if(!r.has("state"))throw new Error(`The url did not contain state parameter ${e}`);if(this.state!==r.get("state"))throw new Error(`The "state" parameter in the url did not match the expected value of ${this.state}`);return{code:r.get("code")}}async getToken(e){const t={grant_type:"authorization_code",code:e.code,redirect_uri:this.redirectUri,client_id:this.client.settings.clientId,code_verifier:e.codeVerifier};return this.client.request("tokenEndpoint",t)}}},443:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.OAuth2Error=void 0;class r extends Error{constructor(e,t,r){super(e),this.oauth2Code=t,this.httpCode=r}}t.OAuth2Error=r},13:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.OAuth2Fetch=void 0,t.OAuth2Fetch=class{constructor(e){this.token=null,this.activeRefresh=null,this.refreshTimer=null,this.options=e,e.getStoredToken&&(async()=>{this.token=await e.getStoredToken()})(),this.scheduleRefresh()}async fetch(e,t){const r=new Request(e,t);return this.fetchMw(r,(e=>fetch(e)))}async fetchMw(e,t){const r=await this.getAccessToken();let n=e.clone();n.headers.set("Authorization","Bearer "+r);let s=await t(n);if(!s.ok&&401===s.status){const r=await this.refreshToken();n=e.clone(),n.headers.set("Authorization","Bearer "+r.accessToken),s=await t(n)}return s}async getToken(){return this.token&&(null===this.token.expiresAt||this.token.expiresAt>Date.now())?this.token:this.refreshToken()}async getAccessToken(){return(await this.getToken()).accessToken}async refreshToken(){var e,t;if(this.activeRefresh)return this.activeRefresh;const r=this.token;this.activeRefresh=(async()=>{var e,t;let n=null;try{(null==r?void 0:r.refreshToken)&&(n=await this.options.client.refreshToken(r))}catch(e){console.warn("[oauth2] refresh token not accepted, we'll try reauthenticating")}if(n||(n=await this.options.getNewToken()),!n){const r=new Error("Unableto obtain OAuth2 tokens, a full reauth may be needed");throw null===(t=(e=this.options).onError)||void 0===t||t.call(e,r),r}return n})();try{const r=await this.activeRefresh;return this.token=r,null===(t=(e=this.options).storeToken)||void 0===t||t.call(e,r),this.scheduleRefresh(),r}catch(e){throw this.options.onError&&this.options.onError(e),e}finally{this.activeRefresh=null}}scheduleRefresh(){if(this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null),!this.token||!this.token.expiresAt||!this.token.refreshToken)return;const e=this.token.expiresAt-Date.now();e<12e4||(this.refreshTimer=setTimeout((async()=>{try{await this.refreshToken()}catch(e){console.error("[fetch-mw-oauth2] error while doing a background OAuth2 auto-refresh",e)}}),e-6e4))}}}},t={};function r(n){var s=t[n];if(void 0!==s)return s.exports;var i=t[n]={exports:{}};return e[n](i,i.exports,r),i.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.OAuth2Error=e.OAuth2Fetch=e.OAuth2Client=void 0;var t=r(934);Object.defineProperty(e,"OAuth2Client",{enumerable:!0,get:function(){return t.OAuth2Client}});var s=r(13);Object.defineProperty(e,"OAuth2Fetch",{enumerable:!0,get:function(){return s.OAuth2Fetch}});var i=r(443);Object.defineProperty(e,"OAuth2Error",{enumerable:!0,get:function(){return i.OAuth2Error}})})(),n})()));
//# sourceMappingURL=fetch-mw-oauth2.min.js.map

@@ -13,6 +13,6 @@ /**

*/
export default class OAuthError extends Error {
oauth2Code: number;
export declare class OAuth2Error extends Error {
oauth2Code: string;
httpCode: number;
constructor(message: string, oauth2Code: number, httpCode: number);
constructor(message: string, oauth2Code: string, httpCode: number);
}
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuth2Error = void 0;
/**

@@ -15,3 +16,3 @@ * An error class for any error the server emits.

*/
class OAuthError extends Error {
class OAuth2Error extends Error {
constructor(message, oauth2Code, httpCode) {

@@ -23,3 +24,3 @@ super(message);

}
exports.default = OAuthError;
exports.OAuth2Error = OAuth2Error;
//# sourceMappingURL=error.js.map

@@ -1,18 +0,41 @@

import { OAuth2Options as Options, OAuth2Token as Token } from './types';
export default class OAuth2 {
options: Options;
token: Token | null;
import { OAuth2Token } from './token';
import { OAuth2Client } from './client';
declare type OAuth2FetchOptions = {
/**
* Keeping track of an active refreshToken operation.
* Reference to OAuth2 client.
*/
client: OAuth2Client;
/**
* You are responsible for implementing this function.
* it's purpose is to supply the 'intitial' oauth2 token.
*
* This will allow us to ensure only 1 such operation happens at any
* given time.
* This function may be async. Return `null` to fail the process.
*/
private activeRefresh;
getNewToken(): OAuth2Token | null | Promise<OAuth2Token | null>;
/**
* Timer trigger for the next automated refresh
* If set, will be called if authenticatin fatally failed.
*/
private refreshTimer;
constructor(options: Options & Partial<Token>, token?: Token | null);
onError?: (err: Error) => void;
/**
* This function is called whenever the active token changes. Using this is
* optional, but it may be used to (for example) put the token in off-line
* storage for later usage.
*/
storeToken?: (token: OAuth2Token) => void;
/**
* Also an optional feature. Implement this if you want the wrapper to try a
* stored token before attempting a full reauthentication.
*
* This function may be async. Return null if there was no token.
*/
getStoredToken?: () => OAuth2Token | null | Promise<OAuth2Token | null>;
};
export declare class OAuth2Fetch {
private options;
/**
* Current active token (if any)
*/
private token;
constructor(options: OAuth2FetchOptions);
/**
* Does a fetch request and adds a Bearer / access token.

@@ -40,4 +63,6 @@ *

* * refreshToken - may be null
*
* This function will attempt to automatically refresh if stale.
*/
getToken(): Promise<Token>;
getToken(): Promise<OAuth2Token>;
/**

@@ -51,6 +76,18 @@ * Returns an access token.

/**
* Keeping track of an active refreshToken operation.
*
* This will allow us to ensure only 1 such operation happens at any
* given time.
*/
private activeRefresh;
/**
* Forces an access token refresh
*/
refreshToken(): Promise<Token>;
refreshToken(): Promise<OAuth2Token>;
/**
* Timer trigger for the next automated refresh
*/
private refreshTimer;
private scheduleRefresh;
}
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("./util");
class OAuth2 {
constructor(options, token = null) {
if (!options.grantType && !token && !options.accessToken) {
throw new Error('If no grantType is specified, a token must be provided');
}
exports.OAuth2Fetch = void 0;
class OAuth2Fetch {
constructor(options) {
/**
* Current active token (if any)
*/
this.token = null;
/**
* Keeping track of an active refreshToken operation.
*
* This will allow us to ensure only 1 such operation happens at any
* given time.
*/
this.activeRefresh = null;
/**
* Timer trigger for the next automated refresh
*/
this.refreshTimer = null;
this.options = options;
// Backwards compatibility
if (options.accessToken) {
// eslint-disable-next-line no-console
console.warn('[fetch-mw-oauth2] Specifying accessToken via the options argument ' +
'in the constructor of OAuth2 is deprecated. Please supply the ' +
'options in the second argument. Backwards compatability will be ' +
'removed in a future version of this library');
token = {
accessToken: options.accessToken,
refreshToken: options.refreshToken || null,
expiresAt: null,
};
if (options.getStoredToken) {
(async () => {
this.token = await options.getStoredToken();
})();
}
this.token = token;
this.activeRefresh = null;
this.refreshTimer = null;
this.scheduleRefresh();

@@ -50,2 +51,3 @@ }

const accessToken = await this.getAccessToken();
// Make a clone. We need to clone if we need to retry the request later.
let authenticatedRequest = request.clone();

@@ -69,2 +71,4 @@ authenticatedRequest.headers.set('Authorization', 'Bearer ' + accessToken);

* * refreshToken - may be null
*
* This function will attempt to automatically refresh if stale.
*/

@@ -92,2 +96,3 @@ async getToken() {

async refreshToken() {
var _a, _b;
if (this.activeRefresh) {

@@ -98,9 +103,38 @@ // If we are currently already doing this operation,

}
this.activeRefresh = util_1.refreshToken(this.options, this.token);
const oldToken = this.token;
this.activeRefresh = (async () => {
var _a, _b;
let newToken = null;
try {
if (oldToken === null || oldToken === void 0 ? void 0 : oldToken.refreshToken) {
// We had a refresh token, lets see if we can use it!
newToken = await this.options.client.refreshToken(oldToken);
}
}
catch (err) {
console.warn('[oauth2] refresh token not accepted, we\'ll try reauthenticating');
}
if (!newToken) {
newToken = await this.options.getNewToken();
}
if (!newToken) {
const err = new Error('Unableto obtain OAuth2 tokens, a full reauth may be needed');
(_b = (_a = this.options).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
throw err;
}
return newToken;
})();
try {
const token = await this.activeRefresh;
this.token = token;
(_b = (_a = this.options).storeToken) === null || _b === void 0 ? void 0 : _b.call(_a, token);
this.scheduleRefresh();
return token;
}
catch (err) {
if (this.options.onError) {
this.options.onError(err);
}
throw err;
}
finally {

@@ -137,3 +171,3 @@ // Make sure we clear the current refresh operation.

}
exports.default = OAuth2;
exports.OAuth2Fetch = OAuth2Fetch;
//# sourceMappingURL=fetch-wrapper.js.map

@@ -1,3 +0,4 @@

export { default as default, default as fetchMwOAuth2, default as OAuth2 } from './fetch-wrapper';
export { OAuth2Options, OAuth2Token } from './types';
export { default as OAuth2Error } from './error';
export { OAuth2Client } from './client';
export { OAuth2Fetch } from './fetch-wrapper';
export { OAuth2Token } from './token';
export { OAuth2Error } from './error';
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuth2Error = exports.OAuth2 = exports.fetchMwOAuth2 = exports.default = void 0;
exports.OAuth2Error = exports.OAuth2Fetch = exports.OAuth2Client = void 0;
var client_1 = require("./client");
Object.defineProperty(exports, "OAuth2Client", { enumerable: true, get: function () { return client_1.OAuth2Client; } });
var fetch_wrapper_1 = require("./fetch-wrapper");
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(fetch_wrapper_1).default; } });
Object.defineProperty(exports, "fetchMwOAuth2", { enumerable: true, get: function () { return __importDefault(fetch_wrapper_1).default; } });
Object.defineProperty(exports, "OAuth2", { enumerable: true, get: function () { return __importDefault(fetch_wrapper_1).default; } });
Object.defineProperty(exports, "OAuth2Fetch", { enumerable: true, get: function () { return fetch_wrapper_1.OAuth2Fetch; } });
var error_1 = require("./error");
Object.defineProperty(exports, "OAuth2Error", { enumerable: true, get: function () { return __importDefault(error_1).default; } });
Object.defineProperty(exports, "OAuth2Error", { enumerable: true, get: function () { return error_1.OAuth2Error; } });
//# sourceMappingURL=index.js.map
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.refreshToken = exports.objToQueryString = void 0;
const base64_1 = require("./base64");
const error_1 = __importDefault(require("./error"));
const error_1 = require("./error");
/**

@@ -89,3 +86,3 @@ * A simple querystring.stringify alternative, so we don't need to include

usesBasicAuth = true;
const basicAuthStr = base64_1.encode(options.clientId + ':' + options.clientSecret);
const basicAuthStr = (0, base64_1.encode)(options.clientId + ':' + options.clientSecret);
headers.Authorization = 'Basic ' + basicAuthStr;

@@ -92,0 +89,0 @@ }

{
"name": "fetch-mw-oauth2",
"version": "1.0.0",
"version": "2.0.0",
"description": "Fetch middleware to add OAuth2 support",

@@ -25,3 +25,3 @@ "main": "dist/index.js",

"devDependencies": {
"@types/node": "^12.20.36",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.2.0",

@@ -28,0 +28,0 @@ "@typescript-eslint/parser": "^5.2.0",

# fetch-mw-oauth2
This library adds support to OAuth2 to fetch by wrapping the fetch function.
It works both for `fetch()` in a browser, as well as [node-fetch][1].
This package contains an OAuth2 client. It aims to be a fully-featured OAuth2
utility library, for Node.js, Browsers and written in Typescript.
This library supports the following features:
* `authorization_code` grant with optional [PKCE][1] support.
* `password` and `client_credentials` grant.
* a `fetch()` wrapper that automatically adds Bearer tokens and refreshes them.
* OAuth2 endpoint discovery via the Server metadata document ([RFC8414][2]).
* OAuth2 Token Introspection ([RFC7662][3]).
## Installation

@@ -14,90 +23,262 @@

The `fetch-mw-oauth2` package effectively works as follows:
To get started, set up the Client class.
1. You pass it OAuth2 instructions
2. It returns an object with a new `fetch()` function.
This new `fetch()` function can now be used in place of the regular fetch,
but it takes responsibility of oauth2 authentication.
```typescript
import { OAuth2Client } from 'fetch-mw-oauth2';
### Setup with access and/or refresh token
const client = new Client({
If you already have an access and/or refresh token obtained through other
means, you can set up the object as such:
// The base URI of your OAuth2 server
server: 'https://my-auth-server/',
```javascript
const { OAuth2 } = require('fetch-mw-oauth2');
// OAuth2 client id
clientId: '...',
const oauth2 = new OAuth2({
clientId: '...',
clientSecret: '...', // Optional in some cases
tokenEndpoint: 'https://auth.example.org/token',
}, {
accessToken: '...',
refreshToken: '...',
// OAuth2 client secret. Only required for 'client_credentials', 'password'
// flows. You should not specify this for authorization_code.
clientSecret: '...',
// The following URIs are all optional. If they are not specified, we will
// attempt to discover them using the oauth2 discovery document.
// If your server doesn't have support this, you may need to specify these.
// you may use relative URIs for any of these.
// Token endpoint. Most flows need this.
// If not specified we'll use the information for the discovery document
// first, and otherwise default to /token
tokenEndpoint: '/token',
// Authorization endpoint.
//
// You only need this to generate URLs for authorization_code flows.
// If not specified we'll use the information for the discovery document
// first, and otherwise default to /authorize
authorizationEndpoint: '/authorize',
// OAuth2 Metadata discovery endpoint.
//
// This document is used to determine various server features.
// If not specified, we assume it's on /.well-known/oauth2-authorization-server
discoveryEndpoint: '/.well-known/oauth2-authorization-server',
});
```
const response = await oauth2.fetch('https://my-api.example.org/articles', {
method: 'POST',
body: 'Hello world',
### Tokens
Many functions use or return a 'OAuth2Token' type. This type has the following
shape:
```typescript
export type OAuth2Token = {
accessToken: string;
refreshToken: string | null;
/**
* When the Access Token expires.
*
* This is expressed as a unix timestamp in milliseconds.
*/
expiresAt: number | null;
};
```
### client_credentials grant.
```typescript
const token = await client.clientCredentials();
```
### Refreshing tokens
```typescript
const newToken = await client.refresh(oldToken);
```
### password grant:
```typescript
const token = await client.password({
username: '..',
password: '..',
});
```
The fetch function simply calls the javascript `fetch()` function but adds
an `Authorization: Bearer ...` header.
### authorization_code
### Setup via authorization_code grant
The `authorization_code` flow is the flow for browser-based applications,
and roughly consists of 3 major steps:
```javascript
const { OAuth2 } = require('fetch-mw-oauth2');
1. Redirect the user to an authorization endpoint, where they log in.
2. Authorization endpoint redirects back to app with a 'code' query
parameter.
3. The `code` is exchanged for a access and refresh token.
const oauth2 = new OAuth2({
grantType: 'authorization_code',
This library provides support for all 3 steps, but there's no requirement
to use its functionality as the system is mostly stateless.
```typescript
import { OAuth2Client } from 'client';
const client = new OAuth2Client({
server: 'https://authserver.example/',
clientId: '...',
code: '...',
redirect_uri: 'https://my-app.example.org/cb',
tokenEndpoint: 'https://auth.example.org/token',
codeVerifier: '...' // If PKCE was used in authorization request
// Note, if urls cannot be auto-detected, also specify these:
tokenEndpoint: '/token',
authorizationEndpoint: '/authorize',
});
const authorizationCode = client.authorizationCode({
// URL in the app that the user should get redirected to after authenticating
redirectUri: 'https://my-app.example/',
// Optional string that can be sent along to the auth server. This value will
// be sent along with the redirect back to the app verbatim.
state: 'some-string',
});
```
The library does not take responsibility for redirecting a user to an
authorization endpoint and redirecting back. That's up to you. After that's
done though, you should have a `code` variable that you can use to setup
the OAuth2 object.
**Redirecting the user to the authorization server**
```typescript
// In a browser this might work as follows:
document.location = await authorizationCode.getAuthorizeUri();
```
### Setup via 'password' grant
**Handling the redirect back to the app and obtain token**
```javascript
const { OAuth2 } = require('fetch-mw-oauth2');
```typescript
const codeResponse = await authorizationCode.validateResponse(
document.location
);
const oauth2 = new OAuth2({
grantType: 'password',
clientId: '...',
clientSecret: '...',
userName: '...',
password: '...',
tokenEndpoint: 'https://auth.example.org/token',
const oauth2Token = await authorizationCode.getToken(codeResponse);
```
### Fetch Wrapper
When using an OAuth2-protected API, typically you will need to obtain an Access
token, and then add this token to each request using an `Authorization: Bearer`
header.
Because access tokens have a limited lifetime, and occasionally needs to be
refreshed this is a bunch of potential plumbing.
To make this easier, this library has a 'fetch wrapper'. This is effectively
just like a regular fetch function, except it automatically adds the header
and will automatically refresh tokens when needed.
Usage:
```typescript
import { OAuth2Client, OAuth2Fetch } from 'fetch-mw-oauth2';
const client = new OAuth2Client({
server: 'https://my-auth-server',
clientId: 'my-client-id'
});
const fetchWrapper = new OAuth2Fetch({
client: client,
/**
* You are responsible for implementing this function.
* it's purpose is to supply the 'intitial' oauth2 token.
*/
getNewToken: async () => {
// Example
return client.clientCredentials();
// Another example
return client.authorizationCode({
code: '..',
redirectUri: '..',
});
// You can return null to fail the process. You may want to do this
// when a user needs to be redirected back to the authorization_code
// endpoints.
return null;
},
/**
* Optional. This will be called for any fatal authentication errors.
*/
onError: (err) => {
// err is of type Error
}
});
```
### Setup via 'client_credentials' grant
After set up, you can just call `fetch` on the new object ot call your API, and
the library will ensure there's always a `Bearer` header.
```javascript
const { OAuth2 } = require('fetch-mw-oauth2');
```typescript
const response = fetchWrapper.fetch('https://my-api', {
method: 'POST',
body: 'Hello world'
});
```
const oauth2 = new OAuth2({
grantType: 'client_credentials',
clientId: '...',
clientSecret: '...',
tokenEndpoint: 'https://auth.example.org/token',
### Storing tokens for later use with FetchWrapper
To keep a user logged in between sessions, you may want to avoid full
reauthentication. To do this, you'll need to store authentication token.
The fetch wrapper has 2 functions to help with this:
```typescript
const fetchWrapper = new OAuth2Fetch({
client: client,
getNewToken: async () => {
// See above!
},
/**
* This function is called whenever the active token changes. Using this is
* optional, but it may be used to (for example) put the token in off-line
* storage for later usage.
*/
storeToken: (token) => {
document.localStorage.setItem('token-store', JSON.stringify(token));
}
/**
* Also an optional feature. Implement this if you want the wrapper to try a
* stored token before attempting a full reauthentication.
*
* This function may be async. Return null if there was no token.
*/
getStoredToken: () => {
const token = document.localStorage.getItem('token-store');
if (token) return JSON.parse(token);
return null;
}
});
```
## fetchMw function
### fetchMw function
It might be preferable to use this library as a more traditional 'middleware'.
The OAuth2 object also exposes a `fetchMw` function that takes 2 arguments:
The OAuth2Fetch object also exposes a `fetchMw` function that takes 2 arguments:

@@ -123,16 +304,37 @@ 1. `request`

## Project status
### Introspection
The current features have been implemented:
Introspection ([RFC7662][3]) lets you find more information about a token,
such as whether it's valid, which user it belongs to, which oauth2 client
was used to generate it, etc.
1. `client_credentials` grant-type support.
2. `password` grant-type support.
3. `authorization_code` grant-type support
4. Automatically refreshing tokens
To be able to use it, your authorization server must have support for the
introspection endpoint. It's location will be automatically detected using
the Metadata discovery document.
The following features are planned mid/long-term
```typescript
import { OAuth2Client } from 'fetch-mw-oauth2';
1. Supply an OAuth2 discovery document instead of authorization and token uris.
2. `implicit` grant-type support
const client = new Client({
server: 'https://auth-server.example/',
[1]: https://www.npmjs.com/package/node-fetch
clientId: '...',
/**
* Some servers require OAuth2 clientId/clientSecret to be passed.
* If they require it, specify it. If not it's fine to omit.
*/
clientSecret: '...',
});
// Get a token
const token = client.clientCredentials();
// Introspect!
console.log(client.introspect(token));
```
[1]: https://datatracker.ietf.org/doc/html/rfc7636 "Proof Key for Code Exchange by OAuth Public Clients"
[2]: https://datatracker.ietf.org/doc/html/rfc8414 "OAuth 2.0 Authorization Server Metadata"
[3]: https://datatracker.ietf.org/doc/html/rfc7662 "OAuth 2.0 Token Introspection"

@@ -13,8 +13,8 @@ /**

*/
export default class OAuthError extends Error {
export class OAuth2Error extends Error {
oauth2Code: number;
oauth2Code: string;
httpCode: number;
constructor(message: string, oauth2Code: number, httpCode: number) {
constructor(message: string, oauth2Code: string, httpCode: number) {

@@ -21,0 +21,0 @@ super(message);

@@ -1,51 +0,61 @@

import {
OAuth2Options as Options,
OAuth2Token as Token
} from './types';
import { refreshToken } from './util';
import { OAuth2Token } from './token';
import { OAuth2Client } from './client';
export default class OAuth2 {
type OAuth2FetchOptions = {
options: Options;
token: Token | null;
/**
* Reference to OAuth2 client.
*/
client: OAuth2Client;
/**
* Keeping track of an active refreshToken operation.
* You are responsible for implementing this function.
* it's purpose is to supply the 'intitial' oauth2 token.
*
* This will allow us to ensure only 1 such operation happens at any
* given time.
* This function may be async. Return `null` to fail the process.
*/
private activeRefresh: Promise<Token> | null;
getNewToken(): OAuth2Token | null | Promise<OAuth2Token | null>;
/**
* Timer trigger for the next automated refresh
* If set, will be called if authenticatin fatally failed.
*/
private refreshTimer: ReturnType<typeof setTimeout> | null;
onError?: (err: Error) => void;
constructor(options: Options & Partial<Token>, token: Token|null = null) {
/**
* This function is called whenever the active token changes. Using this is
* optional, but it may be used to (for example) put the token in off-line
* storage for later usage.
*/
storeToken?: (token: OAuth2Token) => void;
if (!options.grantType && !token && !options.accessToken) {
throw new Error('If no grantType is specified, a token must be provided');
}
this.options = options;
/**
* Also an optional feature. Implement this if you want the wrapper to try a
* stored token before attempting a full reauthentication.
*
* This function may be async. Return null if there was no token.
*/
getStoredToken?: () => OAuth2Token | null | Promise<OAuth2Token | null>;
// Backwards compatibility
if (options.accessToken) {
// eslint-disable-next-line no-console
console.warn(
'[fetch-mw-oauth2] Specifying accessToken via the options argument ' +
'in the constructor of OAuth2 is deprecated. Please supply the ' +
'options in the second argument. Backwards compatability will be ' +
'removed in a future version of this library');
token = {
accessToken: options.accessToken,
refreshToken: options.refreshToken || null,
expiresAt: null,
};
}
}
this.token = token;
this.activeRefresh = null;
this.refreshTimer = null;
export class OAuth2Fetch {
private options: OAuth2FetchOptions;
/**
* Current active token (if any)
*/
private token: OAuth2Token | null = null;
constructor(options: OAuth2FetchOptions) {
this.options = options;
if (options.getStoredToken) {
(async () => {
this.token = await options.getStoredToken!();
})();
}
this.scheduleRefresh();

@@ -86,2 +96,3 @@

// Make a clone. We need to clone if we need to retry the request later.
let authenticatedRequest = request.clone();

@@ -111,4 +122,6 @@ authenticatedRequest.headers.set('Authorization', 'Bearer ' + accessToken);

* * refreshToken - may be null
*
* This function will attempt to automatically refresh if stale.
*/
async getToken(): Promise<Token> {
async getToken(): Promise<OAuth2Token> {

@@ -140,5 +153,14 @@ if (this.token && (this.token.expiresAt === null || this.token.expiresAt > Date.now())) {

/**
* Keeping track of an active refreshToken operation.
*
* This will allow us to ensure only 1 such operation happens at any
* given time.
*/
private activeRefresh: Promise<OAuth2Token> | null = null;
/**
* Forces an access token refresh
*/
async refreshToken(): Promise<Token> {
async refreshToken(): Promise<OAuth2Token> {

@@ -151,12 +173,38 @@ if (this.activeRefresh) {

this.activeRefresh = refreshToken(this.options, this.token);
const oldToken = this.token;
this.activeRefresh = (async() => {
let newToken: OAuth2Token|null = null;
try {
if (oldToken?.refreshToken) {
// We had a refresh token, lets see if we can use it!
newToken = await this.options.client.refreshToken(oldToken);
}
} catch (err) {
console.warn('[oauth2] refresh token not accepted, we\'ll try reauthenticating');
}
if (!newToken) {
newToken = await this.options.getNewToken();
}
if (!newToken) {
const err = new Error('Unableto obtain OAuth2 tokens, a full reauth may be needed');
this.options.onError?.(err);
throw err;
}
return newToken;
})();
try {
const token = await this.activeRefresh;
this.token = token;
this.options.storeToken?.(token);
this.scheduleRefresh();
return token;
} catch (err: any) {
if (this.options.onAuthError) {
this.options.onAuthError(err);
if (this.options.onError) {
this.options.onError(err);
}

@@ -171,2 +219,7 @@ throw err;

/**
* Timer trigger for the next automated refresh
*/
private refreshTimer: ReturnType<typeof setTimeout> | null = null;
private scheduleRefresh() {

@@ -173,0 +226,0 @@

@@ -1,14 +0,4 @@

export {
default as default,
default as fetchMwOAuth2,
default as OAuth2
} from './fetch-wrapper';
export {
OAuth2Options,
OAuth2Token
} from './types';
export {
default as OAuth2Error
}from './error';
export { OAuth2Client } from './client';
export { OAuth2Fetch } from './fetch-wrapper';
export { OAuth2Token } from './token';
export { OAuth2Error } from './error';

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc