@bity/oauth2-auth-code-pkce
Advanced tools
Comparing version 2.0.1 to 2.1.0
328
index.js
/** | ||
* An implementation of rfc6749#section-4.1 and rfc7636. | ||
*/ | ||
var __extends = (this && this.__extends) || (function () { | ||
var extendStatics = function (d, b) { | ||
extendStatics = Object.setPrototypeOf || | ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || | ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; | ||
return extendStatics(d, b); | ||
}; | ||
return function (d, b) { | ||
extendStatics(d, b); | ||
function __() { this.constructor = d; } | ||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); | ||
}; | ||
})(); | ||
var __assign = (this && this.__assign) || function () { | ||
@@ -53,3 +66,168 @@ __assign = Object.assign || function(t) { | ||
; | ||
; | ||
/** | ||
* A list of OAuth2AuthCodePKCE errors. | ||
*/ | ||
// To "namespace" all errors. | ||
var ErrorOAuth2 = /** @class */ (function () { | ||
function ErrorOAuth2() { | ||
} | ||
return ErrorOAuth2; | ||
}()); | ||
export { ErrorOAuth2 }; | ||
// For really unknown errors. | ||
var ErrorUnknown = /** @class */ (function (_super) { | ||
__extends(ErrorUnknown, _super); | ||
function ErrorUnknown() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnknown; | ||
}(ErrorOAuth2)); | ||
export { ErrorUnknown }; | ||
// Some generic, internal errors that can happen. | ||
var ErrorNoAuthCode = /** @class */ (function (_super) { | ||
__extends(ErrorNoAuthCode, _super); | ||
function ErrorNoAuthCode() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorNoAuthCode; | ||
}(ErrorOAuth2)); | ||
export { ErrorNoAuthCode }; | ||
var ErrorInvalidReturnedStateParam = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidReturnedStateParam, _super); | ||
function ErrorInvalidReturnedStateParam() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidReturnedStateParam; | ||
}(ErrorOAuth2)); | ||
export { ErrorInvalidReturnedStateParam }; | ||
var ErrorInvalidJson = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidJson, _super); | ||
function ErrorInvalidJson() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidJson; | ||
}(ErrorOAuth2)); | ||
export { ErrorInvalidJson }; | ||
// Errors that occur across many endpoints | ||
var ErrorInvalidScope = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidScope, _super); | ||
function ErrorInvalidScope() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidScope; | ||
}(ErrorOAuth2)); | ||
export { ErrorInvalidScope }; | ||
var ErrorInvalidRequest = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidRequest, _super); | ||
function ErrorInvalidRequest() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidRequest; | ||
}(ErrorOAuth2)); | ||
export { ErrorInvalidRequest }; | ||
/** | ||
* Possible authorization grant errors given by the redirection from the | ||
* authorization server. | ||
*/ | ||
var ErrorAuthenticationGrant = /** @class */ (function (_super) { | ||
__extends(ErrorAuthenticationGrant, _super); | ||
function ErrorAuthenticationGrant() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorAuthenticationGrant; | ||
}(ErrorOAuth2)); | ||
export { ErrorAuthenticationGrant }; | ||
var ErrorUnauthorizedClient = /** @class */ (function (_super) { | ||
__extends(ErrorUnauthorizedClient, _super); | ||
function ErrorUnauthorizedClient() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnauthorizedClient; | ||
}(ErrorAuthenticationGrant)); | ||
export { ErrorUnauthorizedClient }; | ||
var ErrorAccessDenied = /** @class */ (function (_super) { | ||
__extends(ErrorAccessDenied, _super); | ||
function ErrorAccessDenied() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorAccessDenied; | ||
}(ErrorAuthenticationGrant)); | ||
export { ErrorAccessDenied }; | ||
var ErrorUnsupportedResponseType = /** @class */ (function (_super) { | ||
__extends(ErrorUnsupportedResponseType, _super); | ||
function ErrorUnsupportedResponseType() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnsupportedResponseType; | ||
}(ErrorAuthenticationGrant)); | ||
export { ErrorUnsupportedResponseType }; | ||
var ErrorServerError = /** @class */ (function (_super) { | ||
__extends(ErrorServerError, _super); | ||
function ErrorServerError() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorServerError; | ||
}(ErrorAuthenticationGrant)); | ||
export { ErrorServerError }; | ||
var ErrorTemporarilyUnavailable = /** @class */ (function (_super) { | ||
__extends(ErrorTemporarilyUnavailable, _super); | ||
function ErrorTemporarilyUnavailable() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorTemporarilyUnavailable; | ||
}(ErrorAuthenticationGrant)); | ||
export { ErrorTemporarilyUnavailable }; | ||
/** | ||
* A list of possible access token response errors. | ||
*/ | ||
var ErrorAccessTokenResponse = /** @class */ (function (_super) { | ||
__extends(ErrorAccessTokenResponse, _super); | ||
function ErrorAccessTokenResponse() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorAccessTokenResponse; | ||
}(ErrorOAuth2)); | ||
export { ErrorAccessTokenResponse }; | ||
var ErrorInvalidClient = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidClient, _super); | ||
function ErrorInvalidClient() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidClient; | ||
}(ErrorAccessTokenResponse)); | ||
export { ErrorInvalidClient }; | ||
var ErrorInvalidGrant = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidGrant, _super); | ||
function ErrorInvalidGrant() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidGrant; | ||
}(ErrorAccessTokenResponse)); | ||
export { ErrorInvalidGrant }; | ||
var ErrorUnsupportedGrantType = /** @class */ (function (_super) { | ||
__extends(ErrorUnsupportedGrantType, _super); | ||
function ErrorUnsupportedGrantType() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnsupportedGrantType; | ||
}(ErrorAccessTokenResponse)); | ||
export { ErrorUnsupportedGrantType }; | ||
export var RawErrorToErrorClassMap = { | ||
invalid_request: ErrorInvalidRequest, | ||
invalid_grant: ErrorInvalidGrant, | ||
unauthorized_client: ErrorUnauthorizedClient, | ||
access_denied: ErrorAccessDenied, | ||
unsupported_response_type: ErrorUnsupportedResponseType, | ||
invalid_scope: ErrorInvalidScope, | ||
server_error: ErrorServerError, | ||
temporarily_unavailable: ErrorTemporarilyUnavailable, | ||
invalid_client: ErrorInvalidClient, | ||
unsupported_grant_type: ErrorUnsupportedGrantType, | ||
invalid_json: ErrorInvalidJson, | ||
}; | ||
export function toErrorClass(rawError) { | ||
return new (RawErrorToErrorClassMap[rawError] || ErrorUnknown)(); | ||
} | ||
/** | ||
* To store the OAuth client's data between websites due to redirection. | ||
@@ -87,5 +265,2 @@ */ | ||
this.recoverState(); | ||
if (this.captureGrantCodeAndNotifyIfReturning()) { | ||
this.getAccessToken(); | ||
} | ||
return this; | ||
@@ -104,14 +279,14 @@ } | ||
/** | ||
* If there is an error, it will be passed back as a rejected Promies. | ||
* If there is an error, it will be passed back as a rejected Promise. | ||
* If there is no code, the user should be redirected via | ||
* [fetchAuthorizationGrant]. | ||
* [fetchAuthorizationCode]. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.captureGrantCodeAndNotifyIfReturning = function () { | ||
OAuth2AuthCodePKCE.prototype.isReturningFromAuthServer = function () { | ||
var error = OAuth2AuthCodePKCE.extractParamFromUrl(location.href, 'error'); | ||
if (error) { | ||
return false; | ||
return Promise.reject(toErrorClass(error)); | ||
} | ||
var code = OAuth2AuthCodePKCE.extractParamFromUrl(location.href, 'code'); | ||
if (!code) { | ||
return false; | ||
return Promise.resolve(false); | ||
} | ||
@@ -122,8 +297,9 @@ var state = JSON.parse(localStorage.getItem(LOCALSTORAGE_STATE) || '{}'); | ||
console.warn("state query string parameter doesn't match the one sent! Possible malicious activity somewhere."); | ||
return false; | ||
return Promise.reject(new ErrorInvalidReturnedStateParam()); | ||
} | ||
state.authorizationGrantCode = code; | ||
state.authorizationCode = code; | ||
state.hasAuthCodeBeenExchangedForAccessToken = false; | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(state)); | ||
this.setState(state); | ||
return true; | ||
return Promise.resolve(true); | ||
}; | ||
@@ -133,10 +309,7 @@ /** | ||
* authorization grant code for any reason, but this is non-standard usage. | ||
* | ||
* This method should never return undefined, but was put here to satisfy the | ||
* TypeScript typechecker. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.fetchAccessTokenWithGrant = function (codeOverride) { | ||
OAuth2AuthCodePKCE.prototype.exchangeAuthCodeForAccessToken = function (codeOverride) { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var _a = this.state, _b = _a.authorizationGrantCode, authorizationGrantCode = _b === void 0 ? codeOverride : _b, _c = _a.codeVerifier, codeVerifier = _c === void 0 ? '' : _c; | ||
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; | ||
@@ -146,3 +319,3 @@ if (!codeVerifier) { | ||
} | ||
else if (!authorizationGrantCode) { | ||
else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
@@ -152,3 +325,3 @@ } | ||
var body = "grant_type=authorization_code&" | ||
+ ("code=" + encodeURIComponent(authorizationGrantCode || '') + "&") | ||
+ ("code=" + encodeURIComponent(authorizationCode || '') + "&") | ||
+ ("redirect_uri=" + encodeURIComponent(redirectUrl) + "&") | ||
@@ -164,32 +337,42 @@ + ("client_id=" + encodeURIComponent(clientId) + "&") | ||
}) | ||
.then(function (res) { return res.status === 400 ? Promise.reject(res.json()) : res.json(); }) | ||
.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token; | ||
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 | ||
.then(function (res) { | ||
var jsonPromise = res.json() | ||
.catch(function (jsonDecodeError) { 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.refreshToken = refreshToken; | ||
} | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(_this.state)); | ||
return accessToken; | ||
}) | ||
.catch(function (jsonPromise) { return jsonPromise.then(function (json) { return Promise.reject(json); }); }) | ||
.catch(function (data) { | ||
console.log(data); | ||
var error = data.error || 'There was a network error.'; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this | ||
.fetchAuthorizationGrant() | ||
.catch(function (error) { return console.error(error); }); }); | ||
default: | ||
break; | ||
} | ||
return Promise.reject(error); | ||
_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 }; | ||
}); | ||
}); | ||
@@ -201,3 +384,3 @@ }; | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.fetchAuthorizationGrant = function () { | ||
OAuth2AuthCodePKCE.prototype.fetchAuthorizationCode = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -240,4 +423,2 @@ var _a, clientId, redirectUrl, scopes, _b, codeChallenge, codeVerifier, stateQueryParam, url; | ||
* is easier. | ||
* | ||
* Typically you always want to use this over [fetchAccessTokenWithGrant]. | ||
*/ | ||
@@ -248,21 +429,17 @@ OAuth2AuthCodePKCE.prototype.getAccessToken = function () { | ||
var onAccessTokenExpiry = this.config.onAccessTokenExpiry; | ||
var _a = this.state, accessToken = _a.accessToken, authorizationGrantCode = _a.authorizationGrantCode, refreshToken = _a.refreshToken; | ||
if (!authorizationGrantCode) { | ||
return Promise.reject({ error: 'no_auth_code' }); | ||
var _a = this.state, accessToken = _a.accessToken, authorizationCode = _a.authorizationCode, hasAuthCodeBeenExchangedForAccessToken = _a.hasAuthCodeBeenExchangedForAccessToken, refreshToken = _a.refreshToken, scopes = _a.scopes; | ||
if (!authorizationCode) { | ||
return Promise.reject(new ErrorNoAuthCode()); | ||
} | ||
if (!accessToken) { | ||
if (!accessToken || !hasAuthCodeBeenExchangedForAccessToken) { | ||
console.log('Getting access token with grant'); | ||
return this.fetchAccessTokenWithGrant(); | ||
return this.exchangeAuthCodeForAccessToken(); | ||
} | ||
// If there's no refresh token, attempt with the auth grant code. | ||
if (!refreshToken && (new Date()) >= (new Date(accessToken.expiry))) { | ||
console.log('Renewing access token with grant'); | ||
return onAccessTokenExpiry(function () { return _this.fetchAccessTokenWithGrant(); }); | ||
} | ||
if ((new Date()) >= (new Date(accessToken.expiry))) { | ||
// Depending on the server (and config), refreshToken may not be available. | ||
if (refreshToken && (new Date()) >= (new Date(accessToken.expiry))) { | ||
console.log('Renewing access token with refresh token'); | ||
return onAccessTokenExpiry(function () { return _this.refreshAccessToken(); }); | ||
return onAccessTokenExpiry(function () { return _this.exchangeRefreshTokenForAccessToken(); }); | ||
} | ||
console.log('Access token is accessible and valid'); | ||
return Promise.resolve(accessToken); | ||
return Promise.resolve({ token: accessToken, scopes: scopes }); | ||
}; | ||
@@ -272,3 +449,3 @@ /** | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.refreshAccessToken = function () { | ||
OAuth2AuthCodePKCE.prototype.exchangeRefreshTokenForAccessToken = function () { | ||
var _this = this; | ||
@@ -293,3 +470,4 @@ this.assertStateAndConfigArePresent(); | ||
.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token; | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token, scope = _a.scope; | ||
var scopes = []; | ||
var accessToken = { | ||
@@ -306,4 +484,10 @@ value: access_token, | ||
} | ||
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 accessToken; | ||
return { token: accessToken, scopes: scopes }; | ||
}) | ||
@@ -315,5 +499,4 @@ .catch(function (jsonPromise) { return Promise.reject(jsonPromise); }) | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this | ||
.fetchAuthorizationGrant() | ||
.catch(function (error) { return console.error(error); }); }); | ||
onInvalidGrant(function () { return _this.fetchAuthorizationCode(); }); | ||
break; | ||
default: | ||
@@ -325,2 +508,5 @@ break; | ||
}; | ||
OAuth2AuthCodePKCE.prototype.getGrantedScopes = function () { | ||
return this.state.scopes; | ||
}; | ||
OAuth2AuthCodePKCE.prototype.recoverState = function () { | ||
@@ -336,4 +522,4 @@ this.state = JSON.parse(localStorage.getItem(LOCALSTORAGE_STATE) || '{}'); | ||
/** | ||
* Implements *base64url-encode*, which is NOT the same as regular base64 | ||
* encoding. | ||
* Implements *base64url-encode* (RFC 4648 § 5) without padding, which is NOT | ||
* the same as regular base64 encoding. | ||
*/ | ||
@@ -340,0 +526,0 @@ OAuth2AuthCodePKCE.base64urlEncode = function (value) { |
248
index.ts
@@ -8,4 +8,4 @@ /** | ||
clientId: string; | ||
onAccessTokenExpiry: (refreshAccessToken: () => Promise<AccessToken>) => Promise<AccessToken | undefined>; | ||
onInvalidGrant: (refreshGrantOrRefreshToken: () => Promise<void>) => void; | ||
onAccessTokenExpiry: (refreshAccessToken: () => Promise<AccessContext>) => Promise<AccessContext>; | ||
onInvalidGrant: (refreshAuthCodeOrRefreshToken: () => Promise<void>) => void; | ||
redirectUrl: URL; | ||
@@ -22,8 +22,10 @@ scopes: string[]; | ||
export interface State { | ||
authorizationGrantCode?: string; | ||
accessToken?: AccessToken; | ||
authorizationCode?: string; | ||
codeChallenge?: string; | ||
codeVerifier?: string; | ||
accessToken?: AccessToken; | ||
hasAuthCodeBeenExchangedForAccessToken?: boolean; | ||
refreshToken?: RefreshToken; | ||
stateQueryParam?: string; | ||
scopes?: string[]; | ||
} | ||
@@ -40,5 +42,67 @@ | ||
export type Scopes = string[]; | ||
export interface AccessContext { | ||
token: AccessToken; | ||
scopes: Scopes; | ||
}; | ||
export type URL = string; | ||
/** | ||
* A list of OAuth2AuthCodePKCE errors. | ||
*/ | ||
// To "namespace" all errors. | ||
export class ErrorOAuth2 { } | ||
// For really unknown errors. | ||
export class ErrorUnknown extends ErrorOAuth2 { } | ||
// Some generic, internal errors that can happen. | ||
export class ErrorNoAuthCode extends ErrorOAuth2 { } | ||
export class ErrorInvalidReturnedStateParam extends ErrorOAuth2 { } | ||
export class ErrorInvalidJson extends ErrorOAuth2 { } | ||
// Errors that occur across many endpoints | ||
export class ErrorInvalidScope extends ErrorOAuth2 { } | ||
export class ErrorInvalidRequest extends ErrorOAuth2 { } | ||
/** | ||
* Possible authorization grant errors given by the redirection from the | ||
* authorization server. | ||
*/ | ||
export class ErrorAuthenticationGrant extends ErrorOAuth2 { } | ||
export class ErrorUnauthorizedClient extends ErrorAuthenticationGrant { } | ||
export class ErrorAccessDenied extends ErrorAuthenticationGrant { } | ||
export class ErrorUnsupportedResponseType extends ErrorAuthenticationGrant { } | ||
export class ErrorServerError extends ErrorAuthenticationGrant { } | ||
export class ErrorTemporarilyUnavailable extends ErrorAuthenticationGrant { } | ||
/** | ||
* A list of possible access token response errors. | ||
*/ | ||
export class ErrorAccessTokenResponse extends ErrorOAuth2 { } | ||
export class ErrorInvalidClient extends ErrorAccessTokenResponse { } | ||
export class ErrorInvalidGrant extends ErrorAccessTokenResponse { } | ||
export class ErrorUnsupportedGrantType extends ErrorAccessTokenResponse { } | ||
export const RawErrorToErrorClassMap: { [_: string]: any } = { | ||
invalid_request: ErrorInvalidRequest, | ||
invalid_grant: ErrorInvalidGrant, | ||
unauthorized_client: ErrorUnauthorizedClient, | ||
access_denied: ErrorAccessDenied, | ||
unsupported_response_type: ErrorUnsupportedResponseType, | ||
invalid_scope: ErrorInvalidScope, | ||
server_error: ErrorServerError, | ||
temporarily_unavailable: ErrorTemporarilyUnavailable, | ||
invalid_client: ErrorInvalidClient, | ||
unsupported_grant_type: ErrorUnsupportedGrantType, | ||
invalid_json: ErrorInvalidJson, | ||
}; | ||
export function toErrorClass(rawError: string): ErrorOAuth2 { | ||
return new (RawErrorToErrorClassMap[rawError] || ErrorUnknown)(); | ||
} | ||
/** | ||
* To store the OAuth client's data between websites due to redirection. | ||
@@ -82,5 +146,2 @@ */ | ||
this.recoverState(); | ||
if (this.captureGrantCodeAndNotifyIfReturning()) { | ||
this.getAccessToken(); | ||
} | ||
return this; | ||
@@ -101,10 +162,10 @@ } | ||
/** | ||
* If there is an error, it will be passed back as a rejected Promies. | ||
* If there is an error, it will be passed back as a rejected Promise. | ||
* If there is no code, the user should be redirected via | ||
* [fetchAuthorizationGrant]. | ||
* [fetchAuthorizationCode]. | ||
*/ | ||
private captureGrantCodeAndNotifyIfReturning(): boolean { | ||
private isReturningFromAuthServer(): Promise<boolean> { | ||
const error = OAuth2AuthCodePKCE.extractParamFromUrl(location.href, 'error'); | ||
if (error) { | ||
return false; | ||
return Promise.reject(toErrorClass(error)); | ||
} | ||
@@ -114,3 +175,3 @@ | ||
if (!code) { | ||
return false; | ||
return Promise.resolve(false); | ||
} | ||
@@ -123,26 +184,24 @@ | ||
console.warn("state query string parameter doesn't match the one sent! Possible malicious activity somewhere."); | ||
return false; | ||
return Promise.reject(new ErrorInvalidReturnedStateParam()); | ||
} | ||
state.authorizationGrantCode = code; | ||
state.authorizationCode = code; | ||
state.hasAuthCodeBeenExchangedForAccessToken = false; | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(state)); | ||
this.setState(state); | ||
return true; | ||
return Promise.resolve(true); | ||
} | ||
/** | ||
* 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. | ||
* | ||
* This method should never return undefined, but was put here to satisfy the | ||
* TypeScript typechecker. | ||
*/ | ||
private fetchAccessTokenWithGrant( | ||
private exchangeAuthCodeForAccessToken( | ||
codeOverride?: string | ||
): Promise<AccessToken> { | ||
): Promise<AccessContext> { | ||
this.assertStateAndConfigArePresent(); | ||
const { | ||
authorizationGrantCode = codeOverride, | ||
authorizationCode = codeOverride, | ||
codeVerifier = '' | ||
@@ -154,3 +213,3 @@ } = this.state; | ||
console.warn('No code verifier is being sent.'); | ||
} else if (!authorizationGrantCode) { | ||
} else if (!authorizationCode) { | ||
console.warn('No authorization grant code is being passed.'); | ||
@@ -161,3 +220,3 @@ } | ||
const body = `grant_type=authorization_code&` | ||
+ `code=${encodeURIComponent(authorizationGrantCode || '')}&` | ||
+ `code=${encodeURIComponent(authorizationCode || '')}&` | ||
+ `redirect_uri=${encodeURIComponent(redirectUrl)}&` | ||
@@ -174,34 +233,46 @@ + `client_id=${encodeURIComponent(clientId)}&` | ||
}) | ||
.then(res => res.status === 400 ? Promise.reject(res.json()) : res.json()) | ||
.then(({ access_token, expires_in, refresh_token }) => { | ||
const accessToken: AccessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + (parseInt(expires_in) * 1000))).toString() | ||
}; | ||
this.state.accessToken = accessToken; | ||
.then(res => { | ||
const jsonPromise = res.json() | ||
.catch(jsonDecodeError => ({ error: 'invalid_json' })); | ||
if (refresh_token) { | ||
const refreshToken: RefreshToken = { | ||
value: refresh_token | ||
}; | ||
this.state.refreshToken = refreshToken; | ||
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; | ||
localStorage.setItem(LOCALSTORAGE_STATE, JSON.stringify(this.state)); | ||
return accessToken; | ||
}) | ||
.catch((jsonPromise) => jsonPromise.then((json: any) => Promise.reject(json))) | ||
.catch((data) => { | ||
console.log(data); | ||
const error = data.error || 'There was a network error.'; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(() => this | ||
.fetchAuthorizationGrant() | ||
.catch(error => console.error(error)) | ||
); | ||
default: | ||
break; | ||
} | ||
return Promise.reject(error); | ||
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 }; | ||
}); | ||
}); | ||
@@ -214,3 +285,3 @@ } | ||
*/ | ||
public async fetchAuthorizationGrant(): Promise<void> { | ||
public async fetchAuthorizationCode(): Promise<void> { | ||
this.assertStateAndConfigArePresent(); | ||
@@ -251,32 +322,32 @@ | ||
* is easier. | ||
* | ||
* Typically you always want to use this over [fetchAccessTokenWithGrant]. | ||
*/ | ||
public getAccessToken(): Promise<AccessToken | undefined> { | ||
public getAccessToken(): Promise<AccessContext> { | ||
this.assertStateAndConfigArePresent(); | ||
const { onAccessTokenExpiry } = this.config; | ||
const { accessToken, authorizationGrantCode, refreshToken } = this.state; | ||
if (!authorizationGrantCode) { | ||
return Promise.reject({ error: 'no_auth_code' }); | ||
const { | ||
accessToken, | ||
authorizationCode, | ||
hasAuthCodeBeenExchangedForAccessToken, | ||
refreshToken, | ||
scopes | ||
} = this.state; | ||
if (!authorizationCode) { | ||
return Promise.reject(new ErrorNoAuthCode()); | ||
} | ||
if (!accessToken) { | ||
if (!accessToken || !hasAuthCodeBeenExchangedForAccessToken) { | ||
console.log('Getting access token with grant'); | ||
return this.fetchAccessTokenWithGrant(); | ||
return this.exchangeAuthCodeForAccessToken(); | ||
} | ||
// If there's no refresh token, attempt with the auth grant code. | ||
if (!refreshToken && (new Date()) >= (new Date(accessToken.expiry))) { | ||
console.log('Renewing access token with grant'); | ||
return onAccessTokenExpiry(() => this.fetchAccessTokenWithGrant()); | ||
} | ||
if ((new Date()) >= (new Date(accessToken.expiry))) { | ||
// Depending on the server (and config), refreshToken may not be available. | ||
if (refreshToken && (new Date()) >= (new Date(accessToken.expiry))) { | ||
console.log('Renewing access token with refresh token'); | ||
return onAccessTokenExpiry(() => this.refreshAccessToken()); | ||
return onAccessTokenExpiry(() => this.exchangeRefreshTokenForAccessToken()); | ||
} | ||
console.log('Access token is accessible and valid'); | ||
return Promise.resolve(accessToken); | ||
return Promise.resolve({ token: accessToken, scopes }); | ||
} | ||
@@ -287,5 +358,5 @@ | ||
*/ | ||
public refreshAccessToken(): Promise<AccessToken> { | ||
public exchangeRefreshTokenForAccessToken(): Promise<AccessContext> { | ||
this.assertStateAndConfigArePresent(); | ||
const { onInvalidGrant, tokenUrl } = this.config; | ||
@@ -310,3 +381,5 @@ const { refreshToken } = this.state; | ||
.then(res => res.status === 400 ? Promise.reject(res.json()) : res.json()) | ||
.then(({ access_token, expires_in, refresh_token }) => { | ||
.then(({ access_token, expires_in, refresh_token, scope }) => { | ||
let scopes = []; | ||
const accessToken: AccessToken = { | ||
@@ -317,3 +390,3 @@ value: access_token, | ||
this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
@@ -326,4 +399,11 @@ const 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 accessToken; | ||
return { token: accessToken, scopes }; | ||
}) | ||
@@ -335,6 +415,4 @@ .catch(jsonPromise => Promise.reject(jsonPromise)) | ||
case 'invalid_grant': | ||
onInvalidGrant(() => this | ||
.fetchAuthorizationGrant() | ||
.catch(error => console.error(error)) | ||
); | ||
onInvalidGrant(() => this.fetchAuthorizationCode()); | ||
break; | ||
default: | ||
@@ -347,2 +425,6 @@ break; | ||
public getGrantedScopes(): Scopes { | ||
return this.state.scopes; | ||
} | ||
private recoverState(): this { | ||
@@ -360,4 +442,4 @@ this.state = JSON.parse(localStorage.getItem(LOCALSTORAGE_STATE) || '{}'); | ||
/** | ||
* Implements *base64url-encode*, which is NOT the same as regular base64 | ||
* encoding. | ||
* Implements *base64url-encode* (RFC 4648 § 5) without padding, which is NOT | ||
* the same as regular base64 encoding. | ||
*/ | ||
@@ -364,0 +446,0 @@ static base64urlEncode(value: string): string { |
@@ -1,1 +0,670 @@ | ||
module.exports=function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";var n=this&&this.__assign||function(){return(n=Object.assign||function(e){for(var t,r=1,n=arguments.length;r<n;r++)for(var o in t=arguments[r])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e}).apply(this,arguments)},o=this&&this.__awaiter||function(e,t,r,n){return new(r||(r=Promise))((function(o,a){function i(e){try{s(n.next(e))}catch(e){a(e)}}function c(e){try{s(n.throw(e))}catch(e){a(e)}}function s(e){var t;e.done?o(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(i,c)}s((n=n.apply(e,t||[])).next())}))},a=this&&this.__generator||function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function c(a){return function(c){return function(a){if(r)throw new TypeError("Generator is already executing.");for(;i;)try{if(r=1,n&&(o=2&a[0]?n.return:a[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,a[1])).done)return o;switch(n=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,n=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]<o[3])){i.label=a[1];break}if(6===a[0]&&i.label<o[1]){i.label=o[1],o=a;break}if(o&&i.label<o[2]){i.label=o[2],i.ops.push(a);break}o[2]&&i.ops.pop(),i.trys.pop();continue}a=t.call(e,i)}catch(e){a=[6,e],n=0}finally{r=o=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,c])}}};Object.defineProperty(t,"__esModule",{value:!0}),t.LOCALSTORAGE_ID="oauth2authcodepkce",t.LOCALSTORAGE_STATE=t.LOCALSTORAGE_ID+"-state",t.RECOMMENDED_CODE_VERIFIER_LENGTH=128,t.RECOMMENDED_STATE_LENGTH=32;var i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",c=function(){function e(e){return this.state={},this.config=e,this.recoverState(),this.captureGrantCodeAndNotifyIfReturning()&&this.getAccessToken(),this}return e.prototype.assertStateAndConfigArePresent=function(){if(!this.state||!this.config)throw console.error("state:",this.state,"config:",this.config),new Error("state or config is not set.")},e.prototype.captureGrantCodeAndNotifyIfReturning=function(){if(e.extractParamFromUrl(location.href,"error"))return!1;var r=e.extractParamFromUrl(location.href,"code");if(!r)return!1;var n=JSON.parse(localStorage.getItem(t.LOCALSTORAGE_STATE)||"{}");return e.extractParamFromUrl(location.href,"state")!==n.stateQueryParam?(console.warn("state query string parameter doesn't match the one sent! Possible malicious activity somewhere."),!1):(n.authorizationGrantCode=r,localStorage.setItem(t.LOCALSTORAGE_STATE,JSON.stringify(n)),this.setState(n),!0)},e.prototype.fetchAccessTokenWithGrant=function(e){var r=this;this.assertStateAndConfigArePresent();var n=this.state,o=n.authorizationGrantCode,a=void 0===o?e:o,i=n.codeVerifier,c=void 0===i?"":i,s=this.config,u=s.clientId,f=s.onInvalidGrant,l=s.redirectUrl;c?a||console.warn("No authorization grant code is being passed."):console.warn("No code verifier is being sent.");var h=this.config.tokenUrl,d="grant_type=authorization_code&code="+encodeURIComponent(a||"")+"&redirect_uri="+encodeURIComponent(l)+"&client_id="+encodeURIComponent(u)+"&code_verifier="+c;return fetch(h,{method:"POST",body:d,headers:{"Content-Type":"application/x-www-form-urlencoded"}}).then((function(e){return 400===e.status?Promise.reject(e.json()):e.json()})).then((function(e){var n=e.access_token,o=e.expires_in,a=e.refresh_token,i={value:n,expiry:new Date(Date.now()+1e3*parseInt(o)).toString()};if(r.state.accessToken=i,a){var c={value:a};r.state.refreshToken=c}return localStorage.setItem(t.LOCALSTORAGE_STATE,JSON.stringify(r.state)),i})).catch((function(e){return e.then((function(e){return Promise.reject(e)}))})).catch((function(e){console.log(e);var t=e.error||"There was a network error.";switch(t){case"invalid_grant":f((function(){return r.fetchAuthorizationGrant().catch((function(e){return console.error(e)}))}))}return Promise.reject(t)}))},e.prototype.fetchAuthorizationGrant=function(){return o(this,void 0,void 0,(function(){var r,o,i,c,s,u,f,l,h;return a(this,(function(a){switch(a.label){case 0:return this.assertStateAndConfigArePresent(),r=this.config,o=r.clientId,i=r.redirectUrl,c=r.scopes,[4,e.generatePKCECodes()];case 1:return s=a.sent(),u=s.codeChallenge,f=s.codeVerifier,l=e.generateRandomState(t.RECOMMENDED_STATE_LENGTH),this.state=n(n({},this.state),{codeChallenge:u,codeVerifier:f,stateQueryParam:l}),localStorage.setItem(t.LOCALSTORAGE_STATE,JSON.stringify(this.state)),h=this.config.authorizationUrl+"?response_type=code&client_id="+encodeURIComponent(o)+"&redirect_uri="+encodeURIComponent(i)+"&scope="+encodeURIComponent(c.join(" "))+"&state="+l+"&code_challenge="+encodeURIComponent(u)+"&code_challenge_method=S256",location.replace(h),[2]}}))}))},e.prototype.getAccessToken=function(){var e=this;this.assertStateAndConfigArePresent();var t=this.config.onAccessTokenExpiry,r=this.state,n=r.accessToken,o=r.authorizationGrantCode,a=r.refreshToken;return o?n?!a&&new Date>=new Date(n.expiry)?(console.log("Renewing access token with grant"),t((function(){return e.fetchAccessTokenWithGrant()}))):new Date>=new Date(n.expiry)?(console.log("Renewing access token with refresh token"),t((function(){return e.refreshAccessToken()}))):(console.log("Access token is accessible and valid"),Promise.resolve(n)):(console.log("Getting access token with grant"),this.fetchAccessTokenWithGrant()):Promise.reject({error:"no_auth_code"})},e.prototype.refreshAccessToken=function(){var e=this;this.assertStateAndConfigArePresent();var r=this.config,n=r.onInvalidGrant,o=r.tokenUrl,a=this.state.refreshToken;return a||console.warn("No refresh token is present."),fetch(o,{method:"POST",body:"grant_type=refresh_token&refresh_token="+a,headers:{"Content-Type":"application/x-www-form-urlencoded"}}).then((function(e){return 400===e.status?Promise.reject(e.json()):e.json()})).then((function(r){var n=r.access_token,o=r.expires_in,a=r.refresh_token,i={value:n,expiry:new Date(Date.now()+parseInt(o)).toString()};if(e.state.accessToken=i,a){var c={value:a};e.state.refreshToken=c}return localStorage.setItem(t.LOCALSTORAGE_STATE,JSON.stringify(e.state)),i})).catch((function(e){return Promise.reject(e)})).catch((function(t){var r=t.error||"There was a network error.";switch(r){case"invalid_grant":n((function(){return e.fetchAuthorizationGrant().catch((function(e){return console.error(e)}))}))}return Promise.reject(r)}))},e.prototype.recoverState=function(){return this.state=JSON.parse(localStorage.getItem(t.LOCALSTORAGE_STATE)||"{}"),this},e.prototype.setState=function(e){return this.state=e,localStorage.setItem(t.LOCALSTORAGE_STATE,JSON.stringify(e)),this},e.base64urlEncode=function(e){var t=btoa(e);return t=(t=(t=t.replace(/\+/g,"-")).replace(/\//g,"_")).replace(/=/g,"")},e.extractParamFromUrl=function(e,t){var r=e.split("?");if(r.length<2)return"";var n=r[1].split("&").reduce((function(e,t){return e.concat(t.split("="))}),[]);if(n.length<2)return"";var o=n.indexOf(t);return o>=0?n[o+1]:""},e.generatePKCECodes=function(){var r=new Uint32Array(t.RECOMMENDED_CODE_VERIFIER_LENGTH);crypto.getRandomValues(r);var n=e.base64urlEncode(Array.from(r).map((function(e){return i[e%i.length]})).join(""));return crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n)).then((function(e){for(var t=new Uint8Array(e),r="",n=t.byteLength,o=0;o<n;o++)r+=String.fromCharCode(t[o]);return r})).then(e.base64urlEncode).then((function(e){return{codeChallenge:e,codeVerifier:n}}))},e.generateRandomState=function(e){var t=new Uint32Array(e);return crypto.getRandomValues(t),Array.from(t).map((function(e){return i[e%i.length]})).join("")},e}();t.OAuth2AuthCodePKCE=c}]); | ||
module.exports = | ||
/******/ (function(modules) { // webpackBootstrap | ||
/******/ // The module cache | ||
/******/ var installedModules = {}; | ||
/******/ | ||
/******/ // The require function | ||
/******/ function __webpack_require__(moduleId) { | ||
/******/ | ||
/******/ // Check if module is in cache | ||
/******/ if(installedModules[moduleId]) { | ||
/******/ return installedModules[moduleId].exports; | ||
/******/ } | ||
/******/ // Create a new module (and put it into the cache) | ||
/******/ var module = installedModules[moduleId] = { | ||
/******/ i: moduleId, | ||
/******/ l: false, | ||
/******/ exports: {} | ||
/******/ }; | ||
/******/ | ||
/******/ // Execute the module function | ||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); | ||
/******/ | ||
/******/ // Flag the module as loaded | ||
/******/ module.l = true; | ||
/******/ | ||
/******/ // Return the exports of the module | ||
/******/ return module.exports; | ||
/******/ } | ||
/******/ | ||
/******/ | ||
/******/ // expose the modules object (__webpack_modules__) | ||
/******/ __webpack_require__.m = modules; | ||
/******/ | ||
/******/ // expose the module cache | ||
/******/ __webpack_require__.c = installedModules; | ||
/******/ | ||
/******/ // define getter function for harmony exports | ||
/******/ __webpack_require__.d = function(exports, name, getter) { | ||
/******/ if(!__webpack_require__.o(exports, name)) { | ||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); | ||
/******/ } | ||
/******/ }; | ||
/******/ | ||
/******/ // define __esModule on exports | ||
/******/ __webpack_require__.r = function(exports) { | ||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { | ||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); | ||
/******/ } | ||
/******/ Object.defineProperty(exports, '__esModule', { value: true }); | ||
/******/ }; | ||
/******/ | ||
/******/ // create a fake namespace object | ||
/******/ // mode & 1: value is a module id, require it | ||
/******/ // mode & 2: merge all properties of value into the ns | ||
/******/ // mode & 4: return value when already ns object | ||
/******/ // mode & 8|1: behave like require | ||
/******/ __webpack_require__.t = function(value, mode) { | ||
/******/ if(mode & 1) value = __webpack_require__(value); | ||
/******/ if(mode & 8) return value; | ||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; | ||
/******/ var ns = Object.create(null); | ||
/******/ __webpack_require__.r(ns); | ||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); | ||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); | ||
/******/ return ns; | ||
/******/ }; | ||
/******/ | ||
/******/ // getDefaultExport function for compatibility with non-harmony modules | ||
/******/ __webpack_require__.n = function(module) { | ||
/******/ var getter = module && module.__esModule ? | ||
/******/ function getDefault() { return module['default']; } : | ||
/******/ function getModuleExports() { return module; }; | ||
/******/ __webpack_require__.d(getter, 'a', getter); | ||
/******/ return getter; | ||
/******/ }; | ||
/******/ | ||
/******/ // Object.prototype.hasOwnProperty.call | ||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; | ||
/******/ | ||
/******/ // __webpack_public_path__ | ||
/******/ __webpack_require__.p = ""; | ||
/******/ | ||
/******/ | ||
/******/ // Load entry module and return exports | ||
/******/ return __webpack_require__(__webpack_require__.s = 0); | ||
/******/ }) | ||
/************************************************************************/ | ||
/******/ ([ | ||
/* 0 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
"use strict"; | ||
/** | ||
* An implementation of rfc6749#section-4.1 and rfc7636. | ||
*/ | ||
var __extends = (this && this.__extends) || (function () { | ||
var extendStatics = function (d, b) { | ||
extendStatics = Object.setPrototypeOf || | ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || | ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; | ||
return extendStatics(d, b); | ||
}; | ||
return function (d, b) { | ||
extendStatics(d, b); | ||
function __() { this.constructor = d; } | ||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); | ||
}; | ||
})(); | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
; | ||
; | ||
; | ||
/** | ||
* A list of OAuth2AuthCodePKCE errors. | ||
*/ | ||
// To "namespace" all errors. | ||
var ErrorOAuth2 = /** @class */ (function () { | ||
function ErrorOAuth2() { | ||
} | ||
return ErrorOAuth2; | ||
}()); | ||
exports.ErrorOAuth2 = ErrorOAuth2; | ||
// For really unknown errors. | ||
var ErrorUnknown = /** @class */ (function (_super) { | ||
__extends(ErrorUnknown, _super); | ||
function ErrorUnknown() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnknown; | ||
}(ErrorOAuth2)); | ||
exports.ErrorUnknown = ErrorUnknown; | ||
// Some generic, internal errors that can happen. | ||
var ErrorNoAuthCode = /** @class */ (function (_super) { | ||
__extends(ErrorNoAuthCode, _super); | ||
function ErrorNoAuthCode() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorNoAuthCode; | ||
}(ErrorOAuth2)); | ||
exports.ErrorNoAuthCode = ErrorNoAuthCode; | ||
var ErrorInvalidReturnedStateParam = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidReturnedStateParam, _super); | ||
function ErrorInvalidReturnedStateParam() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidReturnedStateParam; | ||
}(ErrorOAuth2)); | ||
exports.ErrorInvalidReturnedStateParam = ErrorInvalidReturnedStateParam; | ||
var ErrorInvalidJson = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidJson, _super); | ||
function ErrorInvalidJson() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidJson; | ||
}(ErrorOAuth2)); | ||
exports.ErrorInvalidJson = ErrorInvalidJson; | ||
// Errors that occur across many endpoints | ||
var ErrorInvalidScope = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidScope, _super); | ||
function ErrorInvalidScope() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidScope; | ||
}(ErrorOAuth2)); | ||
exports.ErrorInvalidScope = ErrorInvalidScope; | ||
var ErrorInvalidRequest = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidRequest, _super); | ||
function ErrorInvalidRequest() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidRequest; | ||
}(ErrorOAuth2)); | ||
exports.ErrorInvalidRequest = ErrorInvalidRequest; | ||
/** | ||
* Possible authorization grant errors given by the redirection from the | ||
* authorization server. | ||
*/ | ||
var ErrorAuthenticationGrant = /** @class */ (function (_super) { | ||
__extends(ErrorAuthenticationGrant, _super); | ||
function ErrorAuthenticationGrant() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorAuthenticationGrant; | ||
}(ErrorOAuth2)); | ||
exports.ErrorAuthenticationGrant = ErrorAuthenticationGrant; | ||
var ErrorUnauthorizedClient = /** @class */ (function (_super) { | ||
__extends(ErrorUnauthorizedClient, _super); | ||
function ErrorUnauthorizedClient() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnauthorizedClient; | ||
}(ErrorAuthenticationGrant)); | ||
exports.ErrorUnauthorizedClient = ErrorUnauthorizedClient; | ||
var ErrorAccessDenied = /** @class */ (function (_super) { | ||
__extends(ErrorAccessDenied, _super); | ||
function ErrorAccessDenied() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorAccessDenied; | ||
}(ErrorAuthenticationGrant)); | ||
exports.ErrorAccessDenied = ErrorAccessDenied; | ||
var ErrorUnsupportedResponseType = /** @class */ (function (_super) { | ||
__extends(ErrorUnsupportedResponseType, _super); | ||
function ErrorUnsupportedResponseType() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnsupportedResponseType; | ||
}(ErrorAuthenticationGrant)); | ||
exports.ErrorUnsupportedResponseType = ErrorUnsupportedResponseType; | ||
var ErrorServerError = /** @class */ (function (_super) { | ||
__extends(ErrorServerError, _super); | ||
function ErrorServerError() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorServerError; | ||
}(ErrorAuthenticationGrant)); | ||
exports.ErrorServerError = ErrorServerError; | ||
var ErrorTemporarilyUnavailable = /** @class */ (function (_super) { | ||
__extends(ErrorTemporarilyUnavailable, _super); | ||
function ErrorTemporarilyUnavailable() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorTemporarilyUnavailable; | ||
}(ErrorAuthenticationGrant)); | ||
exports.ErrorTemporarilyUnavailable = ErrorTemporarilyUnavailable; | ||
/** | ||
* A list of possible access token response errors. | ||
*/ | ||
var ErrorAccessTokenResponse = /** @class */ (function (_super) { | ||
__extends(ErrorAccessTokenResponse, _super); | ||
function ErrorAccessTokenResponse() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorAccessTokenResponse; | ||
}(ErrorOAuth2)); | ||
exports.ErrorAccessTokenResponse = ErrorAccessTokenResponse; | ||
var ErrorInvalidClient = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidClient, _super); | ||
function ErrorInvalidClient() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidClient; | ||
}(ErrorAccessTokenResponse)); | ||
exports.ErrorInvalidClient = ErrorInvalidClient; | ||
var ErrorInvalidGrant = /** @class */ (function (_super) { | ||
__extends(ErrorInvalidGrant, _super); | ||
function ErrorInvalidGrant() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorInvalidGrant; | ||
}(ErrorAccessTokenResponse)); | ||
exports.ErrorInvalidGrant = ErrorInvalidGrant; | ||
var ErrorUnsupportedGrantType = /** @class */ (function (_super) { | ||
__extends(ErrorUnsupportedGrantType, _super); | ||
function ErrorUnsupportedGrantType() { | ||
return _super !== null && _super.apply(this, arguments) || this; | ||
} | ||
return ErrorUnsupportedGrantType; | ||
}(ErrorAccessTokenResponse)); | ||
exports.ErrorUnsupportedGrantType = ErrorUnsupportedGrantType; | ||
exports.RawErrorToErrorClassMap = { | ||
invalid_request: ErrorInvalidRequest, | ||
invalid_grant: ErrorInvalidGrant, | ||
unauthorized_client: ErrorUnauthorizedClient, | ||
access_denied: ErrorAccessDenied, | ||
unsupported_response_type: ErrorUnsupportedResponseType, | ||
invalid_scope: ErrorInvalidScope, | ||
server_error: ErrorServerError, | ||
temporarily_unavailable: ErrorTemporarilyUnavailable, | ||
invalid_client: ErrorInvalidClient, | ||
unsupported_grant_type: ErrorUnsupportedGrantType, | ||
invalid_json: ErrorInvalidJson, | ||
}; | ||
function toErrorClass(rawError) { | ||
return new (exports.RawErrorToErrorClassMap[rawError] || ErrorUnknown)(); | ||
} | ||
exports.toErrorClass = toErrorClass; | ||
/** | ||
* To store the OAuth client's data between websites due to redirection. | ||
*/ | ||
exports.LOCALSTORAGE_ID = "oauth2authcodepkce"; | ||
exports.LOCALSTORAGE_STATE = exports.LOCALSTORAGE_ID + "-state"; | ||
/** | ||
* The maximum length for a code verifier for the best security we can offer. | ||
*/ | ||
exports.RECOMMENDED_CODE_VERIFIER_LENGTH = 128; | ||
/** | ||
* A sensible length for the state's length, for anti-csrf. | ||
*/ | ||
exports.RECOMMENDED_STATE_LENGTH = 32; | ||
/** | ||
* Character set to generate code verifier defined in rfc7636. | ||
*/ | ||
var PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; | ||
/** | ||
* OAuth 2.0 client that ONLY supports authorization code flow, with PKCE. | ||
* | ||
* Many applications structure their OAuth usage in different ways. This class | ||
* aims to provide both flexible and easy ways to use this configuration of | ||
* OAuth. | ||
* | ||
* See `example.ts` for how you'd typically use this. | ||
* | ||
* For others, review this class's methods. | ||
*/ | ||
var OAuth2AuthCodePKCE = /** @class */ (function () { | ||
function OAuth2AuthCodePKCE(config) { | ||
this.state = {}; | ||
this.config = config; | ||
this.recoverState(); | ||
return this; | ||
} | ||
/** | ||
* 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.'); | ||
} | ||
}; | ||
/** | ||
* If there is an error, it will be passed back as a rejected Promise. | ||
* If there is no code, the user should be redirected via | ||
* [fetchAuthorizationCode]. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.isReturningFromAuthServer = function () { | ||
var error = OAuth2AuthCodePKCE.extractParamFromUrl(location.href, 'error'); | ||
if (error) { | ||
return Promise.reject(toErrorClass(error)); | ||
} | ||
var code = OAuth2AuthCodePKCE.extractParamFromUrl(location.href, 'code'); | ||
if (!code) { | ||
return Promise.resolve(false); | ||
} | ||
var state = JSON.parse(localStorage.getItem(exports.LOCALSTORAGE_STATE) || '{}'); | ||
var stateQueryParam = OAuth2AuthCodePKCE.extractParamFromUrl(location.href, 'state'); | ||
if (stateQueryParam !== state.stateQueryParam) { | ||
console.warn("state query string parameter doesn't match the one sent! Possible malicious activity somewhere."); | ||
return Promise.reject(new ErrorInvalidReturnedStateParam()); | ||
} | ||
state.authorizationCode = code; | ||
state.hasAuthCodeBeenExchangedForAccessToken = false; | ||
localStorage.setItem(exports.LOCALSTORAGE_STATE, JSON.stringify(state)); | ||
this.setState(state); | ||
return Promise.resolve(true); | ||
}; | ||
/** | ||
* 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 (jsonDecodeError) { 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 | ||
* doesn't return because of the redirect behavior (uses `location.replace`). | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.fetchAuthorizationCode = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a, clientId, redirectUrl, scopes, _b, codeChallenge, codeVerifier, stateQueryParam, url; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
this.assertStateAndConfigArePresent(); | ||
_a = this.config, clientId = _a.clientId, redirectUrl = _a.redirectUrl, scopes = _a.scopes; | ||
return [4 /*yield*/, OAuth2AuthCodePKCE | ||
.generatePKCECodes()]; | ||
case 1: | ||
_b = _c.sent(), codeChallenge = _b.codeChallenge, codeVerifier = _b.codeVerifier; | ||
stateQueryParam = OAuth2AuthCodePKCE | ||
.generateRandomState(exports.RECOMMENDED_STATE_LENGTH); | ||
this.state = __assign(__assign({}, this.state), { codeChallenge: codeChallenge, | ||
codeVerifier: codeVerifier, | ||
stateQueryParam: stateQueryParam }); | ||
localStorage.setItem(exports.LOCALSTORAGE_STATE, JSON.stringify(this.state)); | ||
url = this.config.authorizationUrl | ||
+ "?response_type=code&" | ||
+ ("client_id=" + encodeURIComponent(clientId) + "&") | ||
+ ("redirect_uri=" + encodeURIComponent(redirectUrl) + "&") | ||
+ ("scope=" + encodeURIComponent(scopes.join(' ')) + "&") | ||
+ ("state=" + stateQueryParam + "&") | ||
+ ("code_challenge=" + encodeURIComponent(codeChallenge) + "&") | ||
+ "code_challenge_method=S256"; | ||
location.replace(url); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Tries to get the current access token. If there is none | ||
* it will fetch another one. If it is expired, it will fire | ||
* [onAccessTokenExpiry] but it's up to the user to call the refresh token | ||
* function. This is because sometimes not using the refresh token facilities | ||
* is easier. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.getAccessToken = function () { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var onAccessTokenExpiry = this.config.onAccessTokenExpiry; | ||
var _a = this.state, accessToken = _a.accessToken, authorizationCode = _a.authorizationCode, hasAuthCodeBeenExchangedForAccessToken = _a.hasAuthCodeBeenExchangedForAccessToken, refreshToken = _a.refreshToken, scopes = _a.scopes; | ||
if (!authorizationCode) { | ||
return Promise.reject(new ErrorNoAuthCode()); | ||
} | ||
if (!accessToken || !hasAuthCodeBeenExchangedForAccessToken) { | ||
console.log('Getting access token with grant'); | ||
return this.exchangeAuthCodeForAccessToken(); | ||
} | ||
// Depending on the server (and config), refreshToken may not be available. | ||
if (refreshToken && (new Date()) >= (new Date(accessToken.expiry))) { | ||
console.log('Renewing access token with refresh token'); | ||
return onAccessTokenExpiry(function () { return _this.exchangeRefreshTokenForAccessToken(); }); | ||
} | ||
console.log('Access token is accessible and valid'); | ||
return Promise.resolve({ token: accessToken, scopes: scopes }); | ||
}; | ||
/** | ||
* Refresh an access token from the remote service. | ||
*/ | ||
OAuth2AuthCodePKCE.prototype.exchangeRefreshTokenForAccessToken = function () { | ||
var _this = this; | ||
this.assertStateAndConfigArePresent(); | ||
var _a = this.config, onInvalidGrant = _a.onInvalidGrant, tokenUrl = _a.tokenUrl; | ||
var refreshToken = this.state.refreshToken; | ||
if (!refreshToken) { | ||
console.warn('No refresh token is present.'); | ||
} | ||
var url = tokenUrl; | ||
var body = "grant_type=refresh_token&" | ||
+ ("refresh_token=" + refreshToken); | ||
return fetch(url, { | ||
method: 'POST', | ||
body: body, | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
} | ||
}) | ||
.then(function (res) { return res.status === 400 ? Promise.reject(res.json()) : res.json(); }) | ||
.then(function (_a) { | ||
var access_token = _a.access_token, expires_in = _a.expires_in, refresh_token = _a.refresh_token, scope = _a.scope; | ||
var scopes = []; | ||
var accessToken = { | ||
value: access_token, | ||
expiry: (new Date(Date.now() + parseInt(expires_in))).toString() | ||
}; | ||
_this.state.accessToken = accessToken; | ||
if (refresh_token) { | ||
var refreshToken_1 = { | ||
value: refresh_token | ||
}; | ||
_this.state.refreshToken = refreshToken_1; | ||
} | ||
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 }; | ||
}) | ||
.catch(function (jsonPromise) { return Promise.reject(jsonPromise); }) | ||
.catch(function (data) { | ||
var error = data.error || 'There was a network error.'; | ||
switch (error) { | ||
case 'invalid_grant': | ||
onInvalidGrant(function () { return _this.fetchAuthorizationCode(); }); | ||
break; | ||
default: | ||
break; | ||
} | ||
return Promise.reject(error); | ||
}); | ||
}; | ||
OAuth2AuthCodePKCE.prototype.getGrantedScopes = function () { | ||
return this.state.scopes; | ||
}; | ||
OAuth2AuthCodePKCE.prototype.recoverState = function () { | ||
this.state = JSON.parse(localStorage.getItem(exports.LOCALSTORAGE_STATE) || '{}'); | ||
return this; | ||
}; | ||
OAuth2AuthCodePKCE.prototype.setState = function (state) { | ||
this.state = state; | ||
localStorage.setItem(exports.LOCALSTORAGE_STATE, JSON.stringify(state)); | ||
return this; | ||
}; | ||
/** | ||
* Implements *base64url-encode* (RFC 4648 § 5) without padding, which is NOT | ||
* the same as regular base64 encoding. | ||
*/ | ||
OAuth2AuthCodePKCE.base64urlEncode = function (value) { | ||
var base64 = btoa(value); | ||
base64 = base64.replace(/\+/g, '-'); | ||
base64 = base64.replace(/\//g, '_'); | ||
base64 = base64.replace(/=/g, ''); | ||
return base64; | ||
}; | ||
/** | ||
* Extracts a query string parameter. | ||
*/ | ||
OAuth2AuthCodePKCE.extractParamFromUrl = function (url, param) { | ||
var queryString = url.split('?'); | ||
if (queryString.length < 2) { | ||
return ''; | ||
} | ||
var parts = queryString[1] | ||
.split('&') | ||
.reduce(function (a, s) { return a.concat(s.split('=')); }, []); | ||
if (parts.length < 2) { | ||
return ''; | ||
} | ||
var paramIdx = parts.indexOf(param); | ||
return paramIdx >= 0 ? parts[paramIdx + 1] : ''; | ||
}; | ||
/** | ||
* Generates a code_verifier and code_challenge, as specified in rfc7636. | ||
*/ | ||
OAuth2AuthCodePKCE.generatePKCECodes = function () { | ||
var output = new Uint32Array(exports.RECOMMENDED_CODE_VERIFIER_LENGTH); | ||
crypto.getRandomValues(output); | ||
var codeVerifier = OAuth2AuthCodePKCE.base64urlEncode(Array | ||
.from(output) | ||
.map(function (num) { return PKCE_CHARSET[num % PKCE_CHARSET.length]; }) | ||
.join('')); | ||
return crypto | ||
.subtle | ||
.digest('SHA-256', (new TextEncoder()).encode(codeVerifier)) | ||
.then(function (buffer) { | ||
var hash = new Uint8Array(buffer); | ||
var binary = ''; | ||
var hashLength = hash.byteLength; | ||
for (var i = 0; i < hashLength; i++) { | ||
binary += String.fromCharCode(hash[i]); | ||
} | ||
return binary; | ||
}) | ||
.then(OAuth2AuthCodePKCE.base64urlEncode) | ||
.then(function (codeChallenge) { return ({ codeChallenge: codeChallenge, codeVerifier: codeVerifier }); }); | ||
}; | ||
/** | ||
* Generates random state to be passed for anti-csrf. | ||
*/ | ||
OAuth2AuthCodePKCE.generateRandomState = function (lengthOfState) { | ||
var output = new Uint32Array(lengthOfState); | ||
crypto.getRandomValues(output); | ||
return Array | ||
.from(output) | ||
.map(function (num) { return PKCE_CHARSET[num % PKCE_CHARSET.length]; }) | ||
.join(''); | ||
}; | ||
return OAuth2AuthCodePKCE; | ||
}()); | ||
exports.OAuth2AuthCodePKCE = OAuth2AuthCodePKCE; | ||
/***/ }) | ||
/******/ ]); |
{ | ||
"name": "@bity/oauth2-auth-code-pkce", | ||
"version": "2.0.1", | ||
"version": "2.1.0", | ||
"description": "An OAuth 2.0 client that ONLY supports Authorization Code flow with PKCE support.", | ||
@@ -12,3 +12,3 @@ "main": "index.js", | ||
"build": "npm run build:code", | ||
"serve:examples": "http-server ./ --cors" | ||
"serve:tests": "http-server ./ --cors" | ||
}, | ||
@@ -15,0 +15,0 @@ "keywords": [ |
# OAuth2AuthCodePKCE client | ||
A zero dependency OAuth2 client, that **only** support authorization grant | ||
codes, with PKCE support for client side protection. | ||
A zero dependency OAuth 2.0 client supporting *only* the authorization code | ||
grant ([RFC 6749 § 4.1][]) with PKCE ([RFC 7636][]) for client side protection. | ||
[RFC 6749 § 4.1]: https://tools.ietf.org/html/rfc6749#section-4.1 | ||
[RFC 7636]: https://tools.ietf.org/html/rfc7636 | ||
Currently the only Type/JavaScript implementation in public existence. | ||
@@ -16,4 +19,4 @@ | ||
Run `npm run serve:examples` and navigate to | ||
http://localhost:8080/examples/as-an-es6-browser-module.html | ||
Run `npm run serve:tests` and navigate to | ||
http://localhost:8080/tests/panel.html | ||
@@ -26,6 +29,13 @@ This page acts as a test panel for various scenarios. Play around! :) | ||
Module system|File | ||
---|--- | ||
ESModules (import/export)|index.js | ||
CommonJS2 (require e.g. nodejs)|oauth2-auth-code-pkce.js | ||
TypeScript|index.ts | ||
| Module system | File | | ||
|:--------------------------------|:--------------------------| | ||
| ESModules (import/export) | index.js | | ||
| CommonJS2 (require e.g. nodejs) | oauth2-auth-code-pkce.js | | ||
| TypeScript | index.ts | | ||
## Development | ||
### Publishing to NPM | ||
Grab the NPM-generated `bity-oauth2-auth-code-pkce-*.tgz` tarball from CI and | ||
then use `npm publish $tarball` to publish it to NPM. |
const path = require('path'); | ||
module.exports = { | ||
mode: 'none', | ||
entry: './index.ts', | ||
@@ -5,0 +6,0 @@ module: { |
113689
1738
40