@bity/oauth2-auth-code-pkce
Advanced tools
Comparing version 2.1.1 to 2.2.0
239
index.js
@@ -64,2 +64,9 @@ /** | ||
}; | ||
var __spreadArrays = (this && this.__spreadArrays) || function () { | ||
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; | ||
for (var r = Array(s), k = 0, i = 0; i < il; i++) | ||
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) | ||
r[k] = a[j]; | ||
return r; | ||
}; | ||
; | ||
@@ -129,2 +136,10 @@ ; | ||
export { ErrorInvalidRequest }; | ||
var ErrorInvalidToken = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidToken, _super); | ||
function ErrorInvalidToken() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidToken; | ||
}(ErrorOAuth2)); | ||
export { ErrorInvalidToken }; | ||
/** | ||
@@ -217,2 +232,11 @@ * Possible authorization grant errors given by the redirection from the | ||
export { ErrorUnsupportedGrantType }; | ||
/** | ||
* WWW-Authenticate error object structure for less error prone handling. | ||
*/ | ||
var ErrorWWWAuthenticate = /** @class */ (function () { | ||
function ErrorWWWAuthenticate() { | ||
} | ||
return ErrorWWWAuthenticate; | ||
}()); | ||
export { ErrorWWWAuthenticate }; | ||
export var RawErrorToErrorClassMap = { | ||
@@ -230,3 +254,7 @@ invalid_request: ErrorInvalidRequest, | ||
invalid_json: ErrorInvalidJson, | ||
invalid_token: ErrorInvalidToken, | ||
}; | ||
/** | ||
* Translate the raw error strings returned from the server into error classes. | ||
*/ | ||
export function toErrorClass(rawError) { | ||
@@ -236,2 +264,25 @@ return new (RawErrorToErrorClassMap[rawError] || ErrorUnknown)(); | ||
/** | ||
* A convience function to turn, for example, `Bearer realm="bity.com", | ||
* error="invalid_client"` into `{ realm: "bity.com", error: "invalid_client" | ||
* }`. | ||
*/ | ||
export function fromWWWAuthenticateHeaderStringToObject(a) { | ||
var obj = a | ||
.slice("Bearer ".length) | ||
.replace(/"/g, '') | ||
.split(', ') | ||
.map(function (tokens) { | ||
var _a; | ||
var _b = tokens.split('='), k = _b[0], v = _b[1]; | ||
return _a = {}, _a[k] = v, _a; | ||
}) | ||
.reduce(function (a, c) { return (__assign(__assign({}, a), c)); }); | ||
return { realm: obj.realm, error: obj.error }; | ||
} | ||
/** | ||
* HTTP headers that we need to access. | ||
*/ | ||
var HEADER_AUTHORIZATION = "Authorization"; | ||
var HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; | ||
/** | ||
* To store the OAuth client's data between websites due to redirection. | ||
@@ -272,10 +323,34 @@ */ | ||
/** | ||
* If the state or config are missing, it means the client is in a bad state. | ||
* This should never happen, but the check is there just in case. | ||
* Attach the OAuth logic to all fetch requests and translate errors (either | ||
* returned as json or through the WWW-Authenticate header) into nice error | ||
* classes. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.assertStateAndConfigArePresent = function () { | ||
if (!this.state || !this.config) { | ||
console.error('state:', this.state, 'config:', this.config); | ||
throw new Error('state or config is not set.'); | ||
} | ||
OAuth2AuthCodePKCE.prototype.decorateFetchHTTPClient = function (fetch) { | ||
var _this = this; | ||
return function (url, config) { | ||
var rest = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
rest[_i - 2] = arguments[_i]; | ||
} | ||
return _this | ||
.getAccessToken() | ||
.then(function (_a) { | ||
var token = _a.token; | ||
var configNew = Object.assign({}, config); | ||
if (!configNew.headers) { | ||
configNew.headers = {}; | ||
} | ||
configNew.headers[HEADER_AUTHORIZATION] = "Bearer " + token.value; | ||
return fetch.apply(void 0, __spreadArrays([url, configNew], rest)); | ||
}) | ||
.then(function (res) { | ||
if (res.ok) { | ||
return res; | ||
} | ||
if (!res.headers.has(HEADER_WWW_AUTHENTICATE.toLowerCase())) { | ||
return res; | ||
} | ||
return Promise.reject(toErrorClass(fromWWWAuthenticateHeaderStringToObject(res.headers.get(HEADER_WWW_AUTHENTICATE.toLowerCase())).error)); | ||
}); | ||
}; | ||
}; | ||
@@ -309,72 +384,2 @@ /** | ||
/** | ||
* Fetch an access token from the remote service. You may pass a custom | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.exchangeAuthCodeForAccessToken = function (codeOverride) { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var _a = this.state, _b = _a.authorizationCode, authorizationCode = _b === void 0 ? codeOverride : _b, _c = _a.codeVerifier, codeVerifier = _c === void 0 ? '' : _c; | ||
var _d = this.config, clientId = _d.clientId, onInvalidGrant = _d.onInvalidGrant, redirectUrl = _d.redirectUrl; | ||
if (!codeVerifier) { | ||
console.warn('No code verifier is being sent.'); | ||
} | ||
else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
} | ||
var url = this.config.tokenUrl; | ||
var body = "grant_type=authorization_code&" | ||
+ ("code=" + encodeURIComponent(authorizationCode || '') + "&") | ||
+ ("redirect_uri=" + encodeURIComponent(redirectUrl) + "&") | ||
+ ("client_id=" + encodeURIComponent(clientId) + "&") | ||
+ ("code_verifier=" + codeVerifier); | ||
return fetch(url, { | ||
method: 'POST', | ||
body: body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(function (res) { | ||
var jsonPromise = res.json() | ||
.catch(function (_) { return ({ error: 'invalid_json' }); }); | ||
if (!res.ok) { | ||
return jsonPromise.then(function (_a) { | ||
var error = _a.error; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this.fetchAuthorizationCode(); }); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(toErrorClass(error)); | ||
}); | ||
} | ||
return jsonPromise.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token, scope = _a.scope; | ||
var scopes = []; | ||
_this.state.hasAuthCodeBeenExchangedForAccessToken = true; | ||
var accessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
_this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
var refreshToken = { | ||
value: refresh_token | ||
}; | ||
_this.state.refreshToken = refreshToken; | ||
} | ||
if (scope) { | ||
// Multiple scopes are passed and delimited by spaces, | ||
// despite using the singular name "scope". | ||
scopes = scope.split(' '); | ||
_this.state.scopes = scopes; | ||
} | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(_this.state)); | ||
return { token: accessToken, scopes: scopes }; | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Fetch an authorization grant via redirection. In a sense this function | ||
@@ -503,2 +508,82 @@ * doesn't return because of the redirect behavior (uses `location.replace`). | ||
}; | ||
/** | ||
* If the state or config are missing, it means the client is in a bad state. | ||
* This should never happen, but the check is there just in case. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.assertStateAndConfigArePresent = function () { | ||
if (!this.state || !this.config) { | ||
console.error('state:', this.state, 'config:', this.config); | ||
throw new Error('state or config is not set.'); | ||
} | ||
}; | ||
/** | ||
* Fetch an access token from the remote service. You may pass a custom | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.exchangeAuthCodeForAccessToken = function (codeOverride) { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var _a = this.state, _b = _a.authorizationCode, authorizationCode = _b === void 0 ? codeOverride : _b, _c = _a.codeVerifier, codeVerifier = _c === void 0 ? '' : _c; | ||
var _d = this.config, clientId = _d.clientId, onInvalidGrant = _d.onInvalidGrant, redirectUrl = _d.redirectUrl; | ||
if (!codeVerifier) { | ||
console.warn('No code verifier is being sent.'); | ||
} | ||
else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
} | ||
var url = this.config.tokenUrl; | ||
var body = "grant_type=authorization_code&" | ||
+ ("code=" + encodeURIComponent(authorizationCode || '') + "&") | ||
+ ("redirect_uri=" + encodeURIComponent(redirectUrl) + "&") | ||
+ ("client_id=" + encodeURIComponent(clientId) + "&") | ||
+ ("code_verifier=" + codeVerifier); | ||
return fetch(url, { | ||
method: 'POST', | ||
body: body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(function (res) { | ||
var jsonPromise = res.json() | ||
.catch(function (_) { return ({ error: 'invalid_json' }); }); | ||
if (!res.ok) { | ||
return jsonPromise.then(function (_a) { | ||
var error = _a.error; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this.fetchAuthorizationCode(); }); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(toErrorClass(error)); | ||
}); | ||
} | ||
return jsonPromise.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token, scope = _a.scope; | ||
var scopes = []; | ||
_this.state.hasAuthCodeBeenExchangedForAccessToken = true; | ||
var accessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
_this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
var refreshToken = { | ||
value: refresh_token | ||
}; | ||
_this.state.refreshToken = refreshToken; | ||
} | ||
if (scope) { | ||
// Multiple scopes are passed and delimited by spaces, | ||
// despite using the singular name "scope". | ||
scopes = scope.split(' '); | ||
_this.state.scopes = scopes; | ||
} | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(_this.state)); | ||
return { token: accessToken, scopes: scopes }; | ||
}); | ||
}); | ||
}; | ||
OAuth2AuthCodePKCE.prototype.recoverState = function () { | ||
@@ -505,0 +590,0 @@ this.state = JSON.parse(localStorage.getItem(LOCALSTORAGE_STATE) || '{}'); |
253
index.ts
@@ -47,2 +47,3 @@ /** | ||
export type HttpClient = ((...args: any[]) => Promise<any>); | ||
export type URL = string; | ||
@@ -67,2 +68,3 @@ | ||
export class ErrorInvalidRequest extends ErrorOAuth2 { } | ||
export class ErrorInvalidToken extends ErrorOAuth2 { } | ||
@@ -88,2 +90,10 @@ /** | ||
/** | ||
* WWW-Authenticate error object structure for less error prone handling. | ||
*/ | ||
export class ErrorWWWAuthenticate { | ||
public realm: string; | ||
public error: string; | ||
} | ||
export const RawErrorToErrorClassMap: { [_: string]: any } = { | ||
@@ -101,4 +111,8 @@ invalid_request: ErrorInvalidRequest, | ||
invalid_json: ErrorInvalidJson, | ||
invalid_token: ErrorInvalidToken, | ||
}; | ||
/** | ||
* Translate the raw error strings returned from the server into error classes. | ||
*/ | ||
export function toErrorClass(rawError: string): ErrorOAuth2 { | ||
@@ -109,2 +123,26 @@ return new (RawErrorToErrorClassMap[rawError] || ErrorUnknown)(); | ||
/** | ||
* A convience function to turn, for example, `Bearer realm="bity.com", | ||
* error="invalid_client"` into `{ realm: "bity.com", error: "invalid_client" | ||
* }`. | ||
*/ | ||
export function fromWWWAuthenticateHeaderStringToObject( | ||
a: string | ||
): ErrorWWWAuthenticate { | ||
const obj = a | ||
.slice("Bearer ".length) | ||
.replace(/"/g, '') | ||
.split(', ') | ||
.map(tokens => { const [k,v] = tokens.split('='); return {[k]:v}; }) | ||
.reduce((a, c) => ({ ...a, ...c})); | ||
return { realm: obj.realm, error: obj.error }; | ||
} | ||
/** | ||
* HTTP headers that we need to access. | ||
*/ | ||
const HEADER_AUTHORIZATION = "Authorization"; | ||
const HEADER_WWW_AUTHENTICATE= "WWW-Authenticate"; | ||
/** | ||
* To store the OAuth client's data between websites due to redirection. | ||
@@ -152,10 +190,35 @@ */ | ||
/** | ||
* If the state or config are missing, it means the client is in a bad state. | ||
* This should never happen, but the check is there just in case. | ||
* Attach the OAuth logic to all fetch requests and translate errors (either | ||
* returned as json or through the WWW-Authenticate header) into nice error | ||
* classes. | ||
*/ | ||
private assertStateAndConfigArePresent() { | ||
if (!this.state || !this.config) { | ||
console.error('state:', this.state, 'config:', this.config); | ||
throw new Error('state or config is not set.'); | ||
} | ||
public decorateFetchHTTPClient(fetch: HttpClient): HttpClient { | ||
return (url: string, config: any, ...rest) => { | ||
return this | ||
.getAccessToken() | ||
.then(({ token }: AccessContext) => { | ||
const configNew: any = Object.assign({}, config); | ||
if (!configNew.headers) { | ||
configNew.headers = {}; | ||
} | ||
configNew.headers[HEADER_AUTHORIZATION] = `Bearer ${token.value}`; | ||
return fetch(url, configNew, ...rest); | ||
}) | ||
.then((res) => { | ||
if (res.ok) { | ||
return res; | ||
} | ||
if (!res.headers.has(HEADER_WWW_AUTHENTICATE.toLowerCase())) { | ||
return res; | ||
} | ||
return Promise.reject(toErrorClass( | ||
fromWWWAuthenticateHeaderStringToObject( | ||
res.headers.get(HEADER_WWW_AUTHENTICATE.toLowerCase()) | ||
).error | ||
)); | ||
}); | ||
}; | ||
} | ||
@@ -196,84 +259,2 @@ | ||
/** | ||
* Fetch an access token from the remote service. You may pass a custom | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
*/ | ||
private exchangeAuthCodeForAccessToken( | ||
codeOverride?: string | ||
): Promise<AccessContext> { | ||
this.assertStateAndConfigArePresent(); | ||
const { | ||
authorizationCode = codeOverride, | ||
codeVerifier = '' | ||
} = this.state; | ||
const { clientId, onInvalidGrant, redirectUrl } = this.config; | ||
if (!codeVerifier) { | ||
console.warn('No code verifier is being sent.'); | ||
} else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
} | ||
const url = this.config.tokenUrl; | ||
const body = `grant_type=authorization_code&` | ||
+ `code=${encodeURIComponent(authorizationCode || '')}&` | ||
+ `redirect_uri=${encodeURIComponent(redirectUrl)}&` | ||
+ `client_id=${encodeURIComponent(clientId)}&` | ||
+ `code_verifier=${codeVerifier}`; | ||
return fetch(url, { | ||
method: 'POST', | ||
body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(res => { | ||
const jsonPromise = res.json() | ||
.catch(_ => ({ error: 'invalid_json' })); | ||
if (!res.ok) { | ||
return jsonPromise.then(({ error }: any) => { | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(() => this.fetchAuthorizationCode()); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(toErrorClass(error)); | ||
}); | ||
} | ||
return jsonPromise.then(({ access_token, expires_in, refresh_token, scope }) => { | ||
let scopes = []; | ||
this.state.hasAuthCodeBeenExchangedForAccessToken = true; | ||
const accessToken: AccessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
const refreshToken: RefreshToken = { | ||
value: refresh_token | ||
}; | ||
this.state.refreshToken = refreshToken; | ||
} | ||
if (scope) { | ||
// Multiple scopes are passed and delimited by spaces, | ||
// despite using the singular name "scope". | ||
scopes = scope.split(' '); | ||
this.state.scopes = scopes; | ||
} | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(this.state)); | ||
return { token: accessToken, scopes }; | ||
}); | ||
}); | ||
} | ||
/** | ||
* Fetch an authorization grant via redirection. In a sense this function | ||
@@ -419,2 +400,96 @@ * doesn't return because of the redirect behavior (uses `location.replace`). | ||
/** | ||
* If the state or config are missing, it means the client is in a bad state. | ||
* This should never happen, but the check is there just in case. | ||
*/ | ||
private assertStateAndConfigArePresent() { | ||
if (!this.state || !this.config) { | ||
console.error('state:', this.state, 'config:', this.config); | ||
throw new Error('state or config is not set.'); | ||
} | ||
} | ||
/** | ||
* Fetch an access token from the remote service. You may pass a custom | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
*/ | ||
private exchangeAuthCodeForAccessToken( | ||
codeOverride?: string | ||
): Promise<AccessContext> { | ||
this.assertStateAndConfigArePresent(); | ||
const { | ||
authorizationCode = codeOverride, | ||
codeVerifier = '' | ||
} = this.state; | ||
const { clientId, onInvalidGrant, redirectUrl } = this.config; | ||
if (!codeVerifier) { | ||
console.warn('No code verifier is being sent.'); | ||
} else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
} | ||
const url = this.config.tokenUrl; | ||
const body = `grant_type=authorization_code&` | ||
+ `code=${encodeURIComponent(authorizationCode || '')}&` | ||
+ `redirect_uri=${encodeURIComponent(redirectUrl)}&` | ||
+ `client_id=${encodeURIComponent(clientId)}&` | ||
+ `code_verifier=${codeVerifier}`; | ||
return fetch(url, { | ||
method: 'POST', | ||
body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(res => { | ||
const jsonPromise = res.json() | ||
.catch(_ => ({ error: 'invalid_json' })); | ||
if (!res.ok) { | ||
return jsonPromise.then(({ error }: any) => { | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(() => this.fetchAuthorizationCode()); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(toErrorClass(error)); | ||
}); | ||
} | ||
return jsonPromise.then(({ access_token, expires_in, refresh_token, scope }) => { | ||
let scopes = []; | ||
this.state.hasAuthCodeBeenExchangedForAccessToken = true; | ||
const accessToken: AccessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
const refreshToken: RefreshToken = { | ||
value: refresh_token | ||
}; | ||
this.state.refreshToken = refreshToken; | ||
} | ||
if (scope) { | ||
// Multiple scopes are passed and delimited by spaces, | ||
// despite using the singular name "scope". | ||
scopes = scope.split(' '); | ||
this.state.scopes = scopes; | ||
} | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(this.state)); | ||
return { token: accessToken, scopes }; | ||
}); | ||
}); | ||
} | ||
private recoverState(): this { | ||
@@ -421,0 +496,0 @@ this.state = JSON.parse(localStorage.getItem(LOCALSTORAGE_STATE) || '{}'); |
@@ -157,2 +157,9 @@ module.exports = | ||
}; | ||
var __spreadArrays = (this && this.__spreadArrays) || function () { | ||
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; | ||
for (var r = Array(s), k = 0, i = 0; i < il; i++) | ||
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) | ||
r[k] = a[j]; | ||
return r; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -223,2 +230,10 @@ ; | ||
exports.ErrorInvalidRequest = ErrorInvalidRequest; | ||
var ErrorInvalidToken = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidToken, _super); | ||
function ErrorInvalidToken() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidToken; | ||
}(ErrorOAuth2)); | ||
exports.ErrorInvalidToken = ErrorInvalidToken; | ||
/** | ||
@@ -311,2 +326,11 @@ * Possible authorization grant errors given by the redirection from the | ||
exports.ErrorUnsupportedGrantType = ErrorUnsupportedGrantType; | ||
/** | ||
* WWW-Authenticate error object structure for less error prone handling. | ||
*/ | ||
var ErrorWWWAuthenticate = /** @class */ (function () { | ||
function ErrorWWWAuthenticate() { | ||
} | ||
return ErrorWWWAuthenticate; | ||
}()); | ||
exports.ErrorWWWAuthenticate = ErrorWWWAuthenticate; | ||
exports.RawErrorToErrorClassMap = { | ||
@@ -324,3 +348,7 @@ invalid_request: ErrorInvalidRequest, | ||
invalid_json: ErrorInvalidJson, | ||
invalid_token: ErrorInvalidToken, | ||
}; | ||
/** | ||
* Translate the raw error strings returned from the server into error classes. | ||
*/ | ||
function toErrorClass(rawError) { | ||
@@ -331,2 +359,26 @@ return new (exports.RawErrorToErrorClassMap[rawError] || ErrorUnknown)(); | ||
/** | ||
* A convience function to turn, for example, `Bearer realm="bity.com", | ||
* error="invalid_client"` into `{ realm: "bity.com", error: "invalid_client" | ||
* }`. | ||
*/ | ||
function fromWWWAuthenticateHeaderStringToObject(a) { | ||
var obj = a | ||
.slice("Bearer ".length) | ||
.replace(/"/g, '') | ||
.split(', ') | ||
.map(function (tokens) { | ||
var _a; | ||
var _b = tokens.split('='), k = _b[0], v = _b[1]; | ||
return _a = {}, _a[k] = v, _a; | ||
}) | ||
.reduce(function (a, c) { return (__assign(__assign({}, a), c)); }); | ||
return { realm: obj.realm, error: obj.error }; | ||
} | ||
exports.fromWWWAuthenticateHeaderStringToObject = fromWWWAuthenticateHeaderStringToObject; | ||
/** | ||
* HTTP headers that we need to access. | ||
*/ | ||
var HEADER_AUTHORIZATION = "Authorization"; | ||
var HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; | ||
/** | ||
* To store the OAuth client's data between websites due to redirection. | ||
@@ -367,10 +419,34 @@ */ | ||
/** | ||
* If the state or config are missing, it means the client is in a bad state. | ||
* This should never happen, but the check is there just in case. | ||
* Attach the OAuth logic to all fetch requests and translate errors (either | ||
* returned as json or through the WWW-Authenticate header) into nice error | ||
* classes. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.assertStateAndConfigArePresent = function () { | ||
if (!this.state || !this.config) { | ||
console.error('state:', this.state, 'config:', this.config); | ||
throw new Error('state or config is not set.'); | ||
} | ||
OAuth2AuthCodePKCE.prototype.decorateFetchHTTPClient = function (fetch) { | ||
var _this = this; | ||
return function (url, config) { | ||
var rest = []; | ||
for (var _i = 2; _i < arguments.length; _i++) { | ||
rest[_i - 2] = arguments[_i]; | ||
} | ||
return _this | ||
.getAccessToken() | ||
.then(function (_a) { | ||
var token = _a.token; | ||
var configNew = Object.assign({}, config); | ||
if (!configNew.headers) { | ||
configNew.headers = {}; | ||
} | ||
configNew.headers[HEADER_AUTHORIZATION] = "Bearer " + token.value; | ||
return fetch.apply(void 0, __spreadArrays([url, configNew], rest)); | ||
}) | ||
.then(function (res) { | ||
if (res.ok) { | ||
return res; | ||
} | ||
if (!res.headers.has(HEADER_WWW_AUTHENTICATE.toLowerCase())) { | ||
return res; | ||
} | ||
return Promise.reject(toErrorClass(fromWWWAuthenticateHeaderStringToObject(res.headers.get(HEADER_WWW_AUTHENTICATE.toLowerCase())).error)); | ||
}); | ||
}; | ||
}; | ||
@@ -404,72 +480,2 @@ /** | ||
/** | ||
* Fetch an access token from the remote service. You may pass a custom | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.exchangeAuthCodeForAccessToken = function (codeOverride) { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var _a = this.state, _b = _a.authorizationCode, authorizationCode = _b === void 0 ? codeOverride : _b, _c = _a.codeVerifier, codeVerifier = _c === void 0 ? '' : _c; | ||
var _d = this.config, clientId = _d.clientId, onInvalidGrant = _d.onInvalidGrant, redirectUrl = _d.redirectUrl; | ||
if (!codeVerifier) { | ||
console.warn('No code verifier is being sent.'); | ||
} | ||
else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
} | ||
var url = this.config.tokenUrl; | ||
var body = "grant_type=authorization_code&" | ||
+ ("code=" + encodeURIComponent(authorizationCode || '') + "&") | ||
+ ("redirect_uri=" + encodeURIComponent(redirectUrl) + "&") | ||
+ ("client_id=" + encodeURIComponent(clientId) + "&") | ||
+ ("code_verifier=" + codeVerifier); | ||
return fetch(url, { | ||
method: 'POST', | ||
body: body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(function (res) { | ||
var jsonPromise = res.json() | ||
.catch(function (_) { return ({ error: 'invalid_json' }); }); | ||
if (!res.ok) { | ||
return jsonPromise.then(function (_a) { | ||
var error = _a.error; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this.fetchAuthorizationCode(); }); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(toErrorClass(error)); | ||
}); | ||
} | ||
return jsonPromise.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token, scope = _a.scope; | ||
var scopes = []; | ||
_this.state.hasAuthCodeBeenExchangedForAccessToken = true; | ||
var accessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
_this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
var refreshToken = { | ||
value: refresh_token | ||
}; | ||
_this.state.refreshToken = refreshToken; | ||
} | ||
if (scope) { | ||
// Multiple scopes are passed and delimited by spaces, | ||
// despite using the singular name "scope". | ||
scopes = scope.split(' '); | ||
_this.state.scopes = scopes; | ||
} | ||
localStorage.setItem(exports.LOCALSTORAGE_STATE, JSON.stringify(_this.state)); | ||
return { token: accessToken, scopes: scopes }; | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Fetch an authorization grant via redirection. In a sense this function | ||
@@ -598,2 +604,82 @@ * doesn't return because of the redirect behavior (uses `location.replace`). | ||
}; | ||
/** | ||
* If the state or config are missing, it means the client is in a bad state. | ||
* This should never happen, but the check is there just in case. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.assertStateAndConfigArePresent = function () { | ||
if (!this.state || !this.config) { | ||
console.error('state:', this.state, 'config:', this.config); | ||
throw new Error('state or config is not set.'); | ||
} | ||
}; | ||
/** | ||
* Fetch an access token from the remote service. You may pass a custom | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.exchangeAuthCodeForAccessToken = function (codeOverride) { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var _a = this.state, _b = _a.authorizationCode, authorizationCode = _b === void 0 ? codeOverride : _b, _c = _a.codeVerifier, codeVerifier = _c === void 0 ? '' : _c; | ||
var _d = this.config, clientId = _d.clientId, onInvalidGrant = _d.onInvalidGrant, redirectUrl = _d.redirectUrl; | ||
if (!codeVerifier) { | ||
console.warn('No code verifier is being sent.'); | ||
} | ||
else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
} | ||
var url = this.config.tokenUrl; | ||
var body = "grant_type=authorization_code&" | ||
+ ("code=" + encodeURIComponent(authorizationCode || '') + "&") | ||
+ ("redirect_uri=" + encodeURIComponent(redirectUrl) + "&") | ||
+ ("client_id=" + encodeURIComponent(clientId) + "&") | ||
+ ("code_verifier=" + codeVerifier); | ||
return fetch(url, { | ||
method: 'POST', | ||
body: body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(function (res) { | ||
var jsonPromise = res.json() | ||
.catch(function (_) { return ({ error: 'invalid_json' }); }); | ||
if (!res.ok) { | ||
return jsonPromise.then(function (_a) { | ||
var error = _a.error; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this.fetchAuthorizationCode(); }); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(toErrorClass(error)); | ||
}); | ||
} | ||
return jsonPromise.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token, scope = _a.scope; | ||
var scopes = []; | ||
_this.state.hasAuthCodeBeenExchangedForAccessToken = true; | ||
var accessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
_this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
var refreshToken = { | ||
value: refresh_token | ||
}; | ||
_this.state.refreshToken = refreshToken; | ||
} | ||
if (scope) { | ||
// Multiple scopes are passed and delimited by spaces, | ||
// despite using the singular name "scope". | ||
scopes = scope.split(' '); | ||
_this.state.scopes = scopes; | ||
} | ||
localStorage.setItem(exports.LOCALSTORAGE_STATE, JSON.stringify(_this.state)); | ||
return { token: accessToken, scopes: scopes }; | ||
}); | ||
}); | ||
}; | ||
OAuth2AuthCodePKCE.prototype.recoverState = function () { | ||
@@ -600,0 +686,0 @@ this.state = JSON.parse(localStorage.getItem(exports.LOCALSTORAGE_STATE) || '{}'); |
{ | ||
"name": "@bity/oauth2-auth-code-pkce", | ||
"version": "2.1.1", | ||
"version": "2.2.0", | ||
"description": "An OAuth 2.0 client that ONLY supports Authorization Code flow with PKCE support.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
122238
1975
6