fetch-mw-oauth2
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -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", |
336
README.md
# 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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
131863
46
2107
339
8