oauth2-popup-flow
Advanced tools
Comparing version 0.0.1 to 0.1.0
@@ -1,2 +0,2 @@ | ||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.OAuth2PopupFlow=t():e.OAuth2PopupFlow=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";var r=this&&this.__assign||Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},o=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{u(r.next(e))}catch(e){i(e)}}function s(e){try{u(r.throw(e))}catch(e){i(e)}}function u(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}u((r=r.apply(e,t||[])).next())})},i=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=r[2&i[0]?"return":i[0]?"throw":"next"])&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[0,o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e){this.authorizationUrl=e.authorizationUrl,this.clientId=e.clientId,this.redirectUri=e.redirectUri,this.scope=e.scope,this.responseType=e.responseType||"token",this.accessTokenStorageKey=e.accessTokenStorageKey||"token",this.accessTokenResponseKey=e.accessTokenResponseKey||"access_token",this.storage=e.storage||localStorage,this.pollingTime=e.pollingTime||200,this.additionalAuthorizationParameters=e.additionalAuthorizationParameters||{},this.tokenValidator=e.tokenValidator,this.beforePopup=e.beforePopup}return Object.defineProperty(e.prototype,"_rawToken",{get:function(){return this.storage.getItem(this.accessTokenStorageKey)||void 0},set:function(e){null!==e&&void 0!==e&&this.storage.setItem(this.accessTokenStorageKey,e)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"_rawTokenPayload",{get:function(){var t=this._rawToken;if(t){var n=t.split(".")[1];if(n){var r=atob(n);return e.jsonParseOrUndefined(r)}}},enumerable:!0,configurable:!0}),e.prototype.tryLoginPopup=function(){return o(this,void 0,void 0,function(){var t;return i(this,function(n){switch(n.label){case 0:return this.loggedIn()?[2,!0]:this.beforePopup?[4,Promise.resolve(this.beforePopup())]:[3,2];case 1:n.sent(),n.label=2;case 2:return(t=open(this.authorizationUrl+"?"+e.encodeObjectToUri(r({client_id:this.clientId,response_type:this.responseType,redirect_uri:this.redirectUri,scope:this.scope},this.additionalAuthorizationParameters))))?[4,this.authenticated()]:[2,!1];case 3:return n.sent(),t.close(),[2,!0]}})})},e.prototype.logout=function(){this.storage.removeItem(this.accessTokenStorageKey)},e.prototype.loggedIn=function(){var e=this._rawTokenPayload;if(!e)return!1;if(this.tokenValidator){var t=this._rawToken;if(!t)throw new Error("Token was falsy but token payload was not.");if(!this.tokenValidator({payload:e,token:t}))return!1}var n=e.exp;return!!n&&!((new Date).getTime()>1e3*n)},e.prototype.handleRedirect=function(){if(!location.href.startsWith(this.redirectUri))return!1;var t=location.hash;if(!t)return!1;var n=/#(.*)/.exec(t);if(!n)return!1;var r=n[1],o=e.decodeUriToObject(r)[this.accessTokenResponseKey];return!!o&&(this._rawToken=o,!0)},e.prototype.authenticated=function(){return o(this,void 0,void 0,function(){return i(this,function(t){switch(t.label){case 0:return this.loggedIn()?[3,2]:[4,e.time(this.pollingTime)];case 1:return t.sent(),[3,0];case 2:return[2]}})})},e.prototype.token=function(){return o(this,void 0,void 0,function(){var e;return i(this,function(t){switch(t.label){case 0:return this.loggedIn()?[3,2]:(this.tryLoginPopup(),[4,this.authenticated()]);case 1:t.sent(),t.label=2;case 2:if(!(e=this._rawToken))throw new Error("Token was falsy after being authenticated.");return[2,e]}})})},e.prototype.tokenPayload=function(){return o(this,void 0,void 0,function(){var e;return i(this,function(t){switch(t.label){case 0:return this.loggedIn()?[3,2]:(this.tryLoginPopup(),[4,this.authenticated()]);case 1:t.sent(),t.label=2;case 2:if(!(e=this._rawTokenPayload))throw new Error("Token payload was falsy after being authenticated.");return[2,e]}})})},e.jsonParseOrUndefined=function(e){try{return JSON.parse(e)}catch(e){return}},e.time=function(e){return new Promise(function(e){return setTimeout(function(){return e("TIMER")})})},e.decodeUri=function(e){try{return decodeURIComponent(e)}catch(t){return e}},e.encodeObjectToUri=function(e){return Object.keys(e).map(function(t){return{key:t,value:e[t]}}).map(function(e){var t=e.key,n=e.value;return encodeURIComponent(t)+"="+encodeURIComponent(n)}).join("&")},e.decodeUriToObject=function(e){var t=this;return e.split("&").reduce(function(e,n){var r=n.split("="),o=r[0],i=r[1],a=t.decodeUri(o),s=t.decodeUri(i);return e[a]=s,e},{})},e}();t.OAuth2PopupFlow=a}])}); | ||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.OAuth2PopupFlow=t():e.OAuth2PopupFlow=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";var r=this&&this.__assign||Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},o=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{u(r.next(e))}catch(e){i(e)}}function s(e){try{u(r.throw(e))}catch(e){i(e)}}function u(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}u((r=r.apply(e,t||[])).next())})},i=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=r[2&i[0]?"return":i[0]?"throw":"next"])&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[0,o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){a.label=i[1];break}if(6===i[0]&&a.label<o[1]){a.label=o[1],o=i;break}if(o&&a.label<o[2]){a.label=o[2],a.ops.push(i);break}o[2]&&a.ops.pop(),a.trys.pop();continue}i=t.call(e,a)}catch(e){i=[6,e],r=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,s])}}};Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e){this.authorizationUri=e.authorizationUri,this.clientId=e.clientId,this.redirectUri=e.redirectUri,this.scope=e.scope,this.responseType=e.responseType||"token",this.accessTokenStorageKey=e.accessTokenStorageKey||"token",this.accessTokenResponseKey=e.accessTokenResponseKey||"access_token",this.storage=e.storage||window.localStorage,this.pollingTime=e.pollingTime||200,this.additionalAuthorizationParameters=e.additionalAuthorizationParameters,this.tokenValidator=e.tokenValidator,this.beforePopup=e.beforePopup,this.afterResponse=e.afterResponse}return Object.defineProperty(e.prototype,"_rawToken",{get:function(){return this.storage.getItem(this.accessTokenStorageKey)||void 0},set:function(e){null!==e&&void 0!==e&&this.storage.setItem(this.accessTokenStorageKey,e)},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"_rawTokenPayload",{get:function(){var t=this._rawToken;if(t){var n=t.split(".")[1];if(n){var r=window.atob(n);return e.jsonParseOrUndefined(r)}}},enumerable:!0,configurable:!0}),e.prototype.loggedIn=function(){var e=this._rawTokenPayload;if(!e)return!1;if(this.tokenValidator){var t=this._rawToken;if(!this.tokenValidator({payload:e,token:t}))return!1}var n=e.exp;return!!n&&!((new Date).getTime()>1e3*n)},e.prototype.tokenExpired=function(){var e=this._rawTokenPayload;if(!e)return!1;var t=e.exp;return!!t&&!((new Date).getTime()<=1e3*t)},e.prototype.logout=function(){this.storage.removeItem(this.accessTokenStorageKey)},e.prototype.handleRedirect=function(){if(!window.location.href.startsWith(this.redirectUri))return"REDIRECT_URI_MISMATCH";var t=window.location.hash;if(!t)return"FALSY_HASH";var n=/#(.*)/.exec(t);if(!n)return"NO_HASH_MATCH";var r=n[1],o=e.decodeUriToObject(r);this.afterResponse&&this.afterResponse(o);var i=o[this.accessTokenResponseKey];return i?(this._rawToken=i,window.location.hash="","SUCCESS"):"FALSY_TOKEN"},e.prototype.tryLoginPopup=function(){return o(this,void 0,void 0,function(){var t,n;return i(this,function(o){switch(o.label){case 0:return this.loggedIn()?[2,"ALREADY_LOGGED_IN"]:this.beforePopup?[4,Promise.resolve(this.beforePopup())]:[3,2];case 1:o.sent(),o.label=2;case 2:return t="function"==typeof this.additionalAuthorizationParameters?this.additionalAuthorizationParameters():"object"==typeof this.additionalAuthorizationParameters?this.additionalAuthorizationParameters:{},(n=window.open(this.authorizationUri+"?"+e.encodeObjectToUri(r({client_id:this.clientId,response_type:this.responseType,redirect_uri:this.redirectUri,scope:this.scope},t))))?[4,this.authenticated()]:[2,"POPUP_FAILED"];case 3:return o.sent(),n.close(),[2,"SUCCESS"]}})})},e.prototype.authenticated=function(){return o(this,void 0,void 0,function(){return i(this,function(t){switch(t.label){case 0:return this.loggedIn()?[3,2]:[4,e.time(this.pollingTime)];case 1:return t.sent(),[3,0];case 2:return[2]}})})},e.prototype.token=function(){return o(this,void 0,void 0,function(){var e;return i(this,function(t){switch(t.label){case 0:return[4,this.authenticated()];case 1:if(t.sent(),!(e=this._rawToken))throw new Error("Token was falsy after being authenticated.");return[2,e]}})})},e.prototype.tokenPayload=function(){return o(this,void 0,void 0,function(){var e;return i(this,function(t){switch(t.label){case 0:return[4,this.authenticated()];case 1:if(t.sent(),!(e=this._rawTokenPayload))throw new Error("Token payload was falsy after being authenticated.");return[2,e]}})})},e.jsonParseOrUndefined=function(e){try{return JSON.parse(e)}catch(e){return}},e.time=function(e){return new Promise(function(t){return window.setTimeout(function(){return t("TIMER")},e)})},e.decodeUri=function(e){try{return decodeURIComponent(e)}catch(t){return e}},e.encodeObjectToUri=function(e){return Object.keys(e).map(function(t){return{key:t,value:e[t]}}).map(function(e){var t=e.key,n=e.value;return encodeURIComponent(t)+"="+encodeURIComponent(n)}).join("&")},e.decodeUriToObject=function(e){var t=this;return e.split("&").reduce(function(e,n){var r=n.split("="),o=r[0],i=r[1],a=t.decodeUri(o),s=t.decodeUri(i);return e[a]=s,e},{})},e}();t.OAuth2PopupFlow=a}])}); | ||
//# sourceMappingURL=oauth2-popup-flow.js.map |
{ | ||
"name": "oauth2-popup-flow", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"description": "A very simple oauth2 implicit flow library that uses window.open.", | ||
@@ -8,4 +8,5 @@ "main": "./dist/oauth2-popup-flow.js", | ||
"scripts": { | ||
"test": "nyc jasmine-ts \"src/**/*.spec.ts\"", | ||
"build": "webpack -p" | ||
"test": "nyc jasmine-ts \"src/**/*.spec.ts\" | coveralls", | ||
"build": "webpack -p", | ||
"docs": "typedoc --out docs/ src/" | ||
}, | ||
@@ -21,2 +22,3 @@ "author": "Rico Kahler", | ||
"awesome-typescript-loader": "^3.5.0", | ||
"coveralls": "^3.0.0", | ||
"jasmine-spec-reporter": "^4.2.1", | ||
@@ -26,2 +28,3 @@ "jasmine-ts": "^0.2.1", | ||
"nyc": "^11.4.1", | ||
"typedoc": "^0.10.0", | ||
"typescript": "^2.7.2", | ||
@@ -39,10 +42,8 @@ "webpack": "^4.0.1", | ||
"reporter": [ | ||
"lcov", | ||
"text" | ||
"text-lcov" | ||
], | ||
"extension": [ | ||
".ts" | ||
], | ||
"report-dir": "docs" | ||
] | ||
} | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
import { OAuth2PopupFlow, OAuth2PopupFlowOptions } from './'; | ||
import { OAuth2PopupFlow } from './'; | ||
@@ -44,4 +44,17 @@ interface ExampleTokenPayload { | ||
it('calls `setTimeout` and returns `TIMER`', async () => { | ||
const timer = await OAuth2PopupFlow.time(10); | ||
expect(timer).toBe('TIMER'); | ||
function fiveMilliseconds() { | ||
return new Promise<5>(resolve => setTimeout(() => resolve(5), 5)); | ||
} | ||
const race = await Promise.race([ | ||
OAuth2PopupFlow.time(10), | ||
fiveMilliseconds(), | ||
]); | ||
expect(race).toBe(5); | ||
const otherRace = await Promise.race([ | ||
OAuth2PopupFlow.time(0), | ||
fiveMilliseconds(), | ||
]); | ||
expect(otherRace).toBe('TIMER'); | ||
}); | ||
@@ -81,3 +94,4 @@ }); | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
function afterResponse() { } | ||
function tokenValidator() { | ||
return true; | ||
@@ -87,3 +101,3 @@ } | ||
const storage = {} as Storage; | ||
const storage = createTestStorage(); | ||
@@ -94,3 +108,3 @@ const options = { | ||
additionalAuthorizationParameters, | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
@@ -104,2 +118,3 @@ clientId: 'test_client_id', | ||
tokenValidator, | ||
afterResponse, | ||
}; | ||
@@ -112,3 +127,3 @@ | ||
expect(auth.additionalAuthorizationParameters).toBe(additionalAuthorizationParameters); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.authorizationUri).toBe(options.authorizationUri); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
@@ -122,24 +137,10 @@ expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
expect(auth.afterResponse).toBe(afterResponse); | ||
}); | ||
it('uses the default `responseType` of `token` when none is present', () => { | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
return true; | ||
} | ||
const additionalAuthorizationParameters = { foo: 'bar', }; | ||
const storage = {} as Storage; | ||
const options = { | ||
accessTokenResponseKey: 'test_response_key', | ||
accessTokenStorageKey: 'test_storage_key', | ||
additionalAuthorizationParameters, | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'test_client_id', | ||
pollingTime: Math.random(), | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'test scope', | ||
storage, | ||
tokenValidator, | ||
}; | ||
@@ -150,37 +151,9 @@ | ||
expect(auth.responseType).toBe('token'); | ||
// copied from other tests | ||
expect(auth.accessTokenResponseKey).toBe(options.accessTokenResponseKey); | ||
expect(auth.accessTokenStorageKey).toBe(options.accessTokenStorageKey); | ||
expect(auth.additionalAuthorizationParameters).toBe(additionalAuthorizationParameters); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.pollingTime).toBe(options.pollingTime); | ||
expect(auth.redirectUri).toBe(options.redirectUri); | ||
expect(auth.scope).toBe(options.scope); | ||
expect(auth.storage).toBe(storage); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
}); | ||
it('uses the default `accessTokenStorageKey` of `token` when none is present', () => { | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
return true; | ||
} | ||
const additionalAuthorizationParameters = { foo: 'bar', }; | ||
const storage = {} as Storage; | ||
const options = { | ||
accessTokenResponseKey: 'test_response_key', | ||
additionalAuthorizationParameters, | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'test_client_id', | ||
pollingTime: Math.random(), | ||
redirectUri: 'http://localhost:8080/redirect', | ||
responseType: 'test_token', | ||
scope: 'test scope', | ||
storage, | ||
tokenValidator, | ||
}; | ||
@@ -191,37 +164,9 @@ | ||
expect(auth.accessTokenStorageKey).toBe('token'); | ||
// copied from other tests | ||
expect(auth.accessTokenResponseKey).toBe(options.accessTokenResponseKey); | ||
expect(auth.additionalAuthorizationParameters).toBe(additionalAuthorizationParameters); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.pollingTime).toBe(options.pollingTime); | ||
expect(auth.redirectUri).toBe(options.redirectUri); | ||
expect(auth.responseType).toBe(options.responseType); | ||
expect(auth.scope).toBe(options.scope); | ||
expect(auth.storage).toBe(storage); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
}); | ||
it('uses the default `accessTokenResponseKey` of `access_token` when none is present', () => { | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
return true; | ||
} | ||
const additionalAuthorizationParameters = { foo: 'bar', }; | ||
const storage = {} as Storage; | ||
const options = { | ||
accessTokenStorageKey: 'test_storage_key', | ||
additionalAuthorizationParameters, | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'test_client_id', | ||
pollingTime: Math.random(), | ||
redirectUri: 'http://localhost:8080/redirect', | ||
responseType: 'test_token', | ||
scope: 'test scope', | ||
storage, | ||
tokenValidator, | ||
}; | ||
@@ -232,35 +177,9 @@ | ||
expect(auth.accessTokenResponseKey).toBe('access_token'); | ||
// copied from other tests | ||
expect(auth.accessTokenStorageKey).toBe(options.accessTokenStorageKey); | ||
expect(auth.additionalAuthorizationParameters).toBe(additionalAuthorizationParameters); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.pollingTime).toBe(options.pollingTime); | ||
expect(auth.redirectUri).toBe(options.redirectUri); | ||
expect(auth.responseType).toBe(options.responseType); | ||
expect(auth.scope).toBe(options.scope); | ||
expect(auth.storage).toBe(storage); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
}); | ||
it('uses the default `storage` of `localStorage` when none is present', () => { | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
return true; | ||
} | ||
const additionalAuthorizationParameters = { foo: 'bar', }; | ||
it('uses the default `storage` of `window.localStorage` when none is present', () => { | ||
const options = { | ||
accessTokenResponseKey: 'test_response_key', | ||
accessTokenStorageKey: 'test_storage_key', | ||
additionalAuthorizationParameters, | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'test_client_id', | ||
pollingTime: Math.random(), | ||
redirectUri: 'http://localhost:8080/redirect', | ||
responseType: 'test_token', | ||
scope: 'test scope', | ||
tokenValidator, | ||
}; | ||
@@ -271,30 +190,6 @@ | ||
expect(auth.storage).toBe(window.localStorage); | ||
expect(auth.accessTokenResponseKey).toBe(options.accessTokenResponseKey); | ||
expect(auth.accessTokenStorageKey).toBe(options.accessTokenStorageKey); | ||
expect(auth.additionalAuthorizationParameters).toBe(additionalAuthorizationParameters); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.pollingTime).toBe(options.pollingTime); | ||
expect(auth.redirectUri).toBe(options.redirectUri); | ||
expect(auth.responseType).toBe(options.responseType); | ||
expect(auth.scope).toBe(options.scope); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
}); | ||
it('uses the default `pollingTime` of `200` when none is present', () => { | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
return true; | ||
} | ||
const additionalAuthorizationParameters = { foo: 'bar', }; | ||
const storage = {} as Storage; | ||
const options = { | ||
accessTokenResponseKey: 'test_response_key', | ||
accessTokenStorageKey: 'test_storage_key', | ||
additionalAuthorizationParameters, | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'test_client_id', | ||
@@ -304,4 +199,2 @@ redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'test scope', | ||
storage, | ||
tokenValidator, | ||
}; | ||
@@ -312,55 +205,3 @@ | ||
expect(auth.pollingTime).toBe(200); | ||
// copied from other tests | ||
expect(auth.accessTokenResponseKey).toBe(options.accessTokenResponseKey); | ||
expect(auth.accessTokenStorageKey).toBe(options.accessTokenStorageKey); | ||
expect(auth.additionalAuthorizationParameters).toBe(additionalAuthorizationParameters); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.redirectUri).toBe(options.redirectUri); | ||
expect(auth.responseType).toBe(options.responseType); | ||
expect(auth.scope).toBe(options.scope); | ||
expect(auth.storage).toBe(storage); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
}); | ||
it('uses the default `additionalAuthorizationParameters` of `{}` when none is present', () => { | ||
function beforePopup() { } | ||
function tokenValidator(options: { token: string, payload: ExampleTokenPayload }) { | ||
return true; | ||
} | ||
const storage = {} as Storage; | ||
const options = { | ||
accessTokenResponseKey: 'test_response_key', | ||
accessTokenStorageKey: 'test_storage_key', | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
beforePopup, | ||
clientId: 'test_client_id', | ||
pollingTime: Math.random(), | ||
redirectUri: 'http://localhost:8080/redirect', | ||
responseType: 'test_token', | ||
scope: 'test scope', | ||
storage, | ||
tokenValidator, | ||
}; | ||
const auth = new OAuth2PopupFlow(options); | ||
expect(auth.additionalAuthorizationParameters).toEqual({}); | ||
// copied from other tests | ||
expect(auth.accessTokenResponseKey).toBe(options.accessTokenResponseKey); | ||
expect(auth.accessTokenStorageKey).toBe(options.accessTokenStorageKey); | ||
expect(auth.authorizationUrl).toBe(options.authorizationUrl); | ||
expect(auth.beforePopup).toBe(beforePopup); | ||
expect(auth.clientId).toBe(options.clientId); | ||
expect(auth.pollingTime).toBe(options.pollingTime); | ||
expect(auth.redirectUri).toBe(options.redirectUri); | ||
expect(auth.responseType).toBe(options.responseType); | ||
expect(auth.scope).toBe(options.scope); | ||
expect(auth.storage).toBe(storage); | ||
expect(auth.tokenValidator).toBe(tokenValidator); | ||
}); | ||
}); | ||
@@ -373,3 +214,3 @@ | ||
const auth = new OAuth2PopupFlow({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -389,3 +230,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -407,3 +248,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -436,3 +277,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -457,3 +298,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -472,3 +313,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -492,3 +333,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -521,3 +362,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -533,2 +374,35 @@ redirectUri: 'http://localhost:8080/redirect', | ||
}); | ||
it('passes through the `tokenValidator` with `true`', () => { | ||
const storage = createTestStorage(); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
let tokenValidatorCalled = false; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
tokenValidator: ({ token, payload }) => { | ||
expect(token).toBe(exampleToken); | ||
expect(payload).toEqual(examplePayload); | ||
tokenValidatorCalled = true; | ||
return true; | ||
}, | ||
}); | ||
expect(auth.loggedIn()).toBe(true); | ||
expect(tokenValidatorCalled).toBe(true); | ||
}); | ||
it('returns `false` if there is a `tokenValidator` and that returns false', () => { | ||
@@ -551,3 +425,3 @@ const storage = createTestStorage(); | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -583,3 +457,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -608,3 +482,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -633,3 +507,3 @@ redirectUri: 'http://localhost:8080/redirect', | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUrl: 'http://example.com/oauth/authorize', | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
@@ -644,2 +518,588 @@ redirectUri: 'http://localhost:8080/redirect', | ||
}); | ||
describe('tokenExpired', () => { | ||
it('returns `false` if the `_rawTokenPayload` is undefined', () => { | ||
const storage = createTestStorage(); | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
}); | ||
storage._storage.token = undefined; | ||
expect(auth.tokenExpired()).toBe(false); | ||
}); | ||
it('returns `false` if the `exp` in the payload is falsy', () => { | ||
const storage = createTestStorage(); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: 0, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
}); | ||
expect(auth.tokenExpired()).toBe(false); | ||
}); | ||
it('returns `false` if the token is not expired', () => { | ||
const storage = createTestStorage(); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
}); | ||
expect(auth.tokenExpired()).toBe(false); | ||
}); | ||
it('returns `true` if the token is expired', () => { | ||
const storage = createTestStorage(); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) - 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
}); | ||
expect(auth.tokenExpired()).toBe(true); | ||
}); | ||
}); | ||
describe('logout', () => { | ||
it('should remove the token from storage', () => { | ||
const storage = createTestStorage(); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>({ | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
}); | ||
expect(auth.loggedIn()).toBe(true); | ||
auth.logout(); | ||
expect(auth.loggedIn()).toBe(false); | ||
}); | ||
}); | ||
describe('handleRedirect', () => { | ||
it('returns early with `REDIRECT_URI_MISMATCH` if location doesn\'t match the redirect', () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: 'http://localhost:8080/redirect', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
window.location.hash = 'something%20else'; | ||
const result = auth.handleRedirect(); | ||
expect(result).toBe('REDIRECT_URI_MISMATCH'); | ||
}); | ||
it('returns early with `FALSY_HASH` if the hash is falsy', () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
window.location.hash = ''; | ||
const result = auth.handleRedirect(); | ||
expect(result).toBe('FALSY_HASH'); | ||
}); | ||
it('returns early with `NO_HASH_MATCH` if hash doesn\'t match /#(.*)/', () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
window.location.hash = 'shouldn\t match'; | ||
const result = auth.handleRedirect(); | ||
expect(result).toBe('NO_HASH_MATCH'); | ||
}); | ||
it('calls `afterResponse` with the `decodeUriToObject`', () => { | ||
const storage = createTestStorage(); | ||
let afterResponseCalled = false; | ||
const objectToEncode = { | ||
access_token: 'fake access token', | ||
one: 'something', | ||
two: 'something else', | ||
}; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
afterResponse: (obj: { [key: string]: string | undefined }) => { | ||
expect(obj).toEqual(objectToEncode); | ||
afterResponseCalled = true; | ||
} | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
window.location.hash = `#${OAuth2PopupFlow.encodeObjectToUri(objectToEncode)}`; | ||
const result = auth.handleRedirect(); | ||
expect(result).toBe('SUCCESS'); | ||
expect(afterResponseCalled).toBe(true); | ||
}); | ||
it('returns early with `false` if `rawToken` is falsy', () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
window.location.hash = `#${OAuth2PopupFlow.encodeObjectToUri({ | ||
access_token: '', | ||
})}`; | ||
const result = auth.handleRedirect(); | ||
expect(result).toBe('FALSY_TOKEN'); | ||
}); | ||
it('returns `SUCCESS` setting the `_rawToken` and clearing the hash if token is valid', () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
window.location.hash = `#${OAuth2PopupFlow.encodeObjectToUri({ | ||
access_token: 'some token thing', | ||
})}`; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
const result = auth.handleRedirect(); | ||
expect(result).toBe('SUCCESS'); | ||
expect(storage.getItem('token')).toBe('some token thing'); | ||
expect(window.location.hash).toBe(''); | ||
}); | ||
}); | ||
describe('tryLoginPopup', () => { | ||
it('returns `ALREADY_LOGGED_IN` if already `loggedIn()`', async () => { | ||
const storage = createTestStorage(); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(auth.loggedIn()).toBe(true); | ||
expect(await auth.tryLoginPopup()).toBe('ALREADY_LOGGED_IN'); | ||
}); | ||
it('doesn\'t call `beforePopup` if it doesn\'t exist', async () => { | ||
const storage = createTestStorage(); | ||
(window as any).open = () => undefined; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(auth.loggedIn()).toBe(false); | ||
expect(await auth.tryLoginPopup()).toBe('POPUP_FAILED'); | ||
}); | ||
it('calls `beforePopup` synchronously', async () => { | ||
const storage = createTestStorage(); | ||
(window as any).open = () => undefined; | ||
let beforePopupCalled = false; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
beforePopup: () => { | ||
beforePopupCalled = true; | ||
}, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(auth.loggedIn()).toBe(false); | ||
expect(await auth.tryLoginPopup()).toBe('POPUP_FAILED'); | ||
expect(beforePopupCalled).toBe(true); | ||
}); | ||
it('calls `beforePopup` asynchronously', async () => { | ||
const storage = createTestStorage(); | ||
(window as any).open = () => undefined; | ||
let beforePopupCalled = false; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
beforePopup: async () => { | ||
expect(await OAuth2PopupFlow.time(0)).toBe('TIMER'); | ||
beforePopupCalled = true; | ||
}, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(auth.loggedIn()).toBe(false); | ||
expect(await auth.tryLoginPopup()).toBe('POPUP_FAILED'); | ||
expect(beforePopupCalled).toBe(true); | ||
}); | ||
it('calls `additionalAuthorizationParameters` if it is a function', async () => { | ||
const storage = createTestStorage(); | ||
let openCalled = false; | ||
(window as any).open = (url: string) => { | ||
expect(url.includes('foo=bar')).toBe(true); | ||
openCalled = true; | ||
}; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
additionalAuthorizationParameters: () => { | ||
return { | ||
foo: 'bar' | ||
}; | ||
} | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(await auth.tryLoginPopup()).toBe('POPUP_FAILED'); | ||
expect(openCalled).toBe(true); | ||
}); | ||
it('uses `additionalAuthorizationParameters` if it is an object', async () => { | ||
const storage = createTestStorage(); | ||
let openCalled = false; | ||
(window as any).open = (url: string) => { | ||
expect(url.includes('foo=bar')).toBe(true); | ||
openCalled = true; | ||
}; | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
additionalAuthorizationParameters: { foo: 'bar' }, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(await auth.tryLoginPopup()).toBe('POPUP_FAILED'); | ||
expect(openCalled).toBe(true); | ||
}); | ||
it('returns `SUCCESS` and calls `close` on the popup', async () => { | ||
const storage = createTestStorage(); | ||
let closedCalled = false; | ||
(window as any).open = () => ({ | ||
close: () => { | ||
closedCalled = true; | ||
}, | ||
}); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
OAuth2PopupFlow.time(0).then(() => { | ||
storage._storage.token = exampleToken; | ||
}) | ||
expect(auth.loggedIn()).toBe(false); | ||
expect(await auth.tryLoginPopup()).toBe('SUCCESS'); | ||
expect(closedCalled).toBe(true); | ||
}); | ||
}); | ||
describe('authenticated', () => { | ||
it('only resolves after a `loggedIn()` is truthy', async () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
OAuth2PopupFlow.time(10).then(() => { | ||
storage._storage.token = exampleToken; | ||
}); | ||
expect(auth.loggedIn()).toBe(false); | ||
// this won't resolve and the test will fail unless `loggedIn` is truthy | ||
await auth.authenticated(); | ||
}); | ||
}); | ||
describe('token', () => { | ||
it('returns the `_rawToken` if `loggedIn()`', async () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const token = await auth.token(); | ||
expect(token).toEqual(exampleToken); | ||
}); | ||
it('throws if `_rawToken` was falsy after being authenticated', async () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(auth.loggedIn()).toBe(false); | ||
spyOn(auth, 'authenticated'); | ||
let catchCalled = false; | ||
try { | ||
await auth.token(); | ||
} catch (e) { | ||
expect(e.message).toBe('Token was falsy after being authenticated.'); | ||
catchCalled = true; | ||
} finally { | ||
expect(catchCalled).toBe(true); | ||
} | ||
}); | ||
}); | ||
describe('tokenPayload', () => { | ||
it('returns the `_rawToken` if `loggedIn()`', async () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
const examplePayload = { | ||
foo: 'something', | ||
bar: 5, | ||
exp: Math.floor(new Date().getTime() / 1000) + 1000, | ||
}; | ||
const exampleToken = [ | ||
'blah blah header', | ||
window.btoa(JSON.stringify(examplePayload)), | ||
'this is the signature section' | ||
].join('.'); | ||
storage._storage.token = exampleToken; | ||
const payload = await auth.tokenPayload(); | ||
expect(payload).toEqual(examplePayload); | ||
}); | ||
it('throws if `_rawToken` was falsy after being authenticated', async () => { | ||
const storage = createTestStorage(); | ||
const options = { | ||
authorizationUri: 'http://example.com/oauth/authorize', | ||
clientId: 'some_test_client', | ||
redirectUri: '', | ||
scope: 'openid profile', | ||
storage, | ||
}; | ||
const auth = new OAuth2PopupFlow<ExampleTokenPayload>(options); | ||
expect(auth.loggedIn()).toBe(false); | ||
spyOn(auth, 'authenticated'); | ||
let catchCalled = false; | ||
try { | ||
await auth.tokenPayload(); | ||
} catch (e) { | ||
expect(e.message).toBe('Token payload was falsy after being authenticated.'); | ||
catchCalled = true; | ||
} finally { | ||
expect(catchCalled).toBe(true); | ||
} | ||
}); | ||
}); | ||
}); |
316
src/index.ts
@@ -0,18 +1,198 @@ | ||
/** | ||
* The type of the configuration object used to create a `OAuth2PopupFlow` | ||
* | ||
* Each property has a JSDOC description to explain what it does. | ||
*/ | ||
export interface OAuth2PopupFlowOptions<TokenPayload extends { exp: number }> { | ||
authorizationUrl: string, | ||
/** | ||
* REQUIRED | ||
* The full URI of the authorization endpoint provided by the authorization server. | ||
* | ||
* e.g. `https://example.com/oauth/authorize` | ||
*/ | ||
authorizationUri: string, | ||
/** | ||
* REQUIRED | ||
* The client ID of your application provided by the authorization server. | ||
* | ||
* This client ID is sent to the authorization server using `authorizationUrl` endpoint in the | ||
* query portion of the URL along with the other parameters. | ||
* This value will be URL encoded like so: | ||
* | ||
* `https://example.com/oauth/authorize?client_id=SOME_CLIENT_ID_VALUE...` | ||
*/ | ||
clientId: string, | ||
/** | ||
* REQUIRED | ||
* The URI that the authorization server will to redirect after the user has been authenticated. | ||
* This redirect URI *must* be a URI from *your application* and it must also be registered with | ||
* the authorization server. Some authorities call this a "callback URLs" or "login URLs" etc. | ||
* | ||
* > e.g. `http://localhost:4200/redirect` for local testing | ||
* > | ||
* > or `https://my-application.com/redirect` for prod | ||
* | ||
* This redirect URI is sent to the authorization server using `authorizationUrl` endpoint in the | ||
* query portion of the URL along with the other parameters. | ||
* This value will be URL encoded like so: | ||
* | ||
* `https://example.com/oauth/authorize?redirect_URI=http%3A%2F%2Flocalhost%2Fredirect...` | ||
*/ | ||
redirectUri: string, | ||
/** | ||
* REQUIRED | ||
* A list permission separated by spaces that is the scope of permissions your application is | ||
* requesting from the authorization server. If the user is logging in the first time, it may ask | ||
* them to approve those permission before authorizing your application. | ||
* | ||
* > e.g. `openid profile` | ||
* | ||
* The scopes are sent to the authorization server using `authorizationUrl` endpoint in the | ||
* query portion of the URL along with the other parameters. | ||
* This value will be URL encoded like so: | ||
* | ||
* `https://example.com/oauth/authorize?scope=openid%20profile...` | ||
*/ | ||
scope: string, | ||
/** | ||
* OPTIONAL | ||
* `response_type` is an argument to be passed to the authorization server via the | ||
* `authorizationUri` endpoint in the query portion of the URL. | ||
* | ||
* Most implementations of oauth2 use the default value of `token` to tell the authorization | ||
* server to start the implicit grant flow but you may override that value with this option. | ||
* | ||
* For example, Auth0--an OAuth2 authority/authorization server--requires the value `id_token` | ||
* instead of `token` for the implicit flow. | ||
* | ||
* The response type is sent to the authorization server using `authorizationUrl` endpoint in the | ||
* query portion of the URL along with the other parameters. | ||
* This value will be URL encoded like so: | ||
* | ||
* `https://example.com/oauth/authorize?response_type=token...` | ||
*/ | ||
responseType?: string, | ||
/** | ||
* OPTIONAL | ||
* The key used to save the token in the given storage. The default key is `token` so the token | ||
* would be persisted in `localStorage.getItem('token')` if `localStorage` was the configured | ||
* `Storage`. | ||
*/ | ||
accessTokenStorageKey?: string, | ||
/** | ||
* OPTIONAL | ||
* During `handleRedirect`, the method will try to parse `window.location.hash` to an object using | ||
* `OAuth2PopupFlow.decodeUriToObject`. After that object has been decoded, this property | ||
* determines the key to use that will retrieve the token from that object. | ||
* | ||
* By default it is `access_token` but you you may need to change that e.g. Auth0 uses `id_token`. | ||
*/ | ||
accessTokenResponseKey?: string, | ||
/** | ||
* OPTIONAL | ||
* The storage implementation of choice. It can be `localStorage` or `sessionStorage` or something | ||
* else. By default, this is `localStorage` and `localStorage` is the preferred `Storage`. | ||
*/ | ||
storage?: Storage, | ||
/** | ||
* OPTIONAL | ||
* The `authenticated` method periodically checks `loggedIn()` and resolves when `loggedIn()` | ||
* returns `true`. | ||
* | ||
* This property is how long it will wait between checks. By default it is `200`. | ||
*/ | ||
pollingTime?: number, | ||
additionalAuthorizationParameters?: { [key: string]: string }, | ||
/** | ||
* OPTIONAL | ||
* Some oauth authorities require additional parameters to be passed to the `authorizationUri` | ||
* URL in order for the implicit grant flow to work. | ||
* | ||
* For example: [Auth0--an OAuth2 authority/authorization server--requires the parameters | ||
* `nonce`][0] | ||
* be passed along with every call to the `authorizationUri`. You can do that like so: | ||
* | ||
* ```ts | ||
* const auth = new OAuth2PopupFlow({ | ||
* authorizationUri: 'https://example.com/oauth/authorize', | ||
* clientId: 'foo_client', | ||
* redirectUri: 'http://localhost:8080/redirect', | ||
* scope: 'openid profile', | ||
* // this can be a function or static object | ||
* additionalAuthorizationParameters: () => { | ||
* // in prod, consider something more cryptographic | ||
* const nonce = Math.floor(Math.random() * 1000).toString(); | ||
* localStorage.setItem('nonce', nonce); | ||
* return { nonce }; | ||
* // `nonce` will now be encoded in the URL like so: | ||
* // https://example.com/oauth/authorize?client_id=foo_client...nonce=1234 | ||
* }, | ||
* // the token returned by Auth0, has the `nonce` in the payload | ||
* // you can add this additional check now | ||
* tokenValidator: ({ payload }) => { | ||
* const storageNonce = parseInt(localStorage.getItem('nonce'), 10); | ||
* const payloadNonce = parseInt(payload.nonce, 10); | ||
* return storageNonce === payloadNonce; | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* [0]: https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce | ||
*/ | ||
additionalAuthorizationParameters?: (() => { [key: string]: string }) | { [key: string]: string }, | ||
/** | ||
* OPTIONAL | ||
* This function intercepts the `loggedIn` method and causes it to return early with `false` if | ||
* this function itself returns `false`. Use this function to validate claims in the token payload | ||
* or token. | ||
* | ||
* [For example: validating the `nonce`:][0] | ||
* | ||
* ```ts | ||
* const auth = new OAuth2PopupFlow({ | ||
* authorizationUri: 'https://example.com/oauth/authorize', | ||
* clientId: 'foo_client', | ||
* redirectUri: 'http://localhost:8080/redirect', | ||
* scope: 'openid profile', | ||
* // this can be a function or static object | ||
* additionalAuthorizationParameters: () => { | ||
* // in prod, consider something more cryptographic | ||
* const nonce = Math.floor(Math.random() * 1000).toString(); | ||
* localStorage.setItem('nonce', nonce); | ||
* return { nonce }; | ||
* // `nonce` will now be encoded in the URL like so: | ||
* // https://example.com/oauth/authorize?client_id=foo_client...nonce=1234 | ||
* }, | ||
* // the token returned by Auth0, has the `nonce` in the payload | ||
* // you can add this additional check now | ||
* tokenValidator: ({ payload }) => { | ||
* const storageNonce = parseInt(localStorage.getItem('nonce'), 10); | ||
* const payloadNonce = parseInt(payload.nonce, 10); | ||
* return storageNonce === payloadNonce; | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* [0]: https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce | ||
*/ | ||
tokenValidator?: (options: { payload: TokenPayload, token: string }) => boolean, | ||
/** | ||
* OPTIONAL | ||
* A hook that runs in `tryLoginPopup` before any popup is opened. This function can return a | ||
* `Promise` and the popup will not open until it resolves. | ||
* | ||
* A typical use case would be to wait a certain amount of time before opening the popup to let | ||
* the user see why the popup is happening. | ||
*/ | ||
beforePopup?: () => any | Promise<any>, | ||
/** | ||
* OPTIONAL | ||
* A hook that runs in `handleRedirect` that takes in the result of the hash payload from the | ||
* authorization server. Use this hook to grab more from the response or to debug the response | ||
* from the authorization URL. | ||
*/ | ||
afterResponse?: (authorizationResponse: { [key: string]: string | undefined }) => void, | ||
} | ||
export class OAuth2PopupFlow<TokenPayload extends { exp: number }> { | ||
authorizationUrl: string; | ||
authorizationUri: string; | ||
clientId: string; | ||
@@ -26,8 +206,9 @@ redirectUri: string; | ||
pollingTime: number; | ||
additionalAuthorizationParameters: { [key: string]: string }; | ||
additionalAuthorizationParameters?: (() => { [key: string]: string }) | { [key: string]: string }; | ||
tokenValidator?: (options: { payload: TokenPayload, token: string }) => boolean; | ||
beforePopup?: () => any | Promise<any>; | ||
afterResponse?: (authorizationResponse: { [key: string]: string | undefined }) => void; | ||
constructor(options: OAuth2PopupFlowOptions<TokenPayload>) { | ||
this.authorizationUrl = options.authorizationUrl; | ||
this.authorizationUri = options.authorizationUri; | ||
this.clientId = options.clientId; | ||
@@ -41,5 +222,6 @@ this.redirectUri = options.redirectUri; | ||
this.pollingTime = options.pollingTime || 200; | ||
this.additionalAuthorizationParameters = options.additionalAuthorizationParameters || {}; | ||
this.additionalAuthorizationParameters = options.additionalAuthorizationParameters; | ||
this.tokenValidator = options.tokenValidator; | ||
this.beforePopup = options.beforePopup; | ||
this.afterResponse = options.afterResponse; | ||
} | ||
@@ -71,2 +253,6 @@ | ||
/** | ||
* A simple synchronous method that returns whether or not the user is logged in by checking | ||
* whether or not their token is present and not expired. | ||
*/ | ||
loggedIn() { | ||
@@ -84,3 +270,3 @@ const decodedPayload = this._rawTokenPayload; | ||
if (new Date().getTime() > exp * 1000) { return false; } | ||
if (new Date().getTime() > (exp * 1000)) { return false; } | ||
@@ -90,2 +276,22 @@ return true; | ||
/** | ||
* Returns true only if there is a token in storage and that token is expired. Use this to method | ||
* in conjunction with `loggedIn` to display a message like "you need to *re*login" vs "you need | ||
* to login". | ||
*/ | ||
tokenExpired() { | ||
const decodedPayload = this._rawTokenPayload; | ||
if (!decodedPayload) { return false; } | ||
const exp = decodedPayload.exp; | ||
if (!exp) { return false; } | ||
if (new Date().getTime() <= (exp * 1000)) { return false; } | ||
return true; | ||
} | ||
/** | ||
* Deletes the token from the given storage causing `loggedIn` to return false on its next call. | ||
*/ | ||
logout() { | ||
@@ -95,20 +301,37 @@ this.storage.removeItem(this.accessTokenStorageKey); | ||
/** | ||
* Call this method in a route of the `redirectUri`. This method takes the value of the hash at | ||
* `window.location.hash` and attempts to grab the token from the URL. | ||
* | ||
* If the method was able to grab the token, it will return `'SUCCESS'` else it will return a | ||
* different string. | ||
*/ | ||
handleRedirect() { | ||
const locationHref = location.href; | ||
if (!locationHref.startsWith(this.redirectUri)) { return false; } | ||
const rawHash = location.hash; | ||
if (!rawHash) { return false; } | ||
const locationHref = window.location.href; | ||
if (!locationHref.startsWith(this.redirectUri)) { return 'REDIRECT_URI_MISMATCH'; } | ||
const rawHash = window.location.hash; | ||
if (!rawHash) { return 'FALSY_HASH'; } | ||
const hashMatch = /#(.*)/.exec(rawHash); | ||
if (!hashMatch) { return false; } | ||
if (!hashMatch) { return 'NO_HASH_MATCH'; } | ||
const hash = hashMatch[1]; | ||
const authorizationResponse = OAuth2PopupFlow.decodeUriToObject(hash); | ||
if (this.afterResponse) { | ||
this.afterResponse(authorizationResponse); | ||
} | ||
const rawToken = authorizationResponse[this.accessTokenResponseKey]; | ||
if (!rawToken) { return false; } | ||
if (!rawToken) { return 'FALSY_TOKEN'; } | ||
this._rawToken = rawToken; | ||
return true; | ||
window.location.hash = ''; | ||
return 'SUCCESS'; | ||
} | ||
/** | ||
* Tries to open a popup to login the user in. If the user is already `loggedIn()` it will | ||
* immediately return `'ALREADY_LOGGED_IN'`. If the popup fails to open, it will immediately | ||
* return `'POPUP_FAILED'` else it will wait for `loggedIn()` to be `true` and eventually | ||
* return `'SUCCESS'`. | ||
*/ | ||
async tryLoginPopup() { | ||
if (this.loggedIn()) { return true; } | ||
if (this.loggedIn()) { return 'ALREADY_LOGGED_IN'; } | ||
@@ -119,3 +342,10 @@ if (this.beforePopup) { | ||
const popup = open(`${this.authorizationUrl}?${OAuth2PopupFlow.encodeObjectToUri({ | ||
const additionalParams = (/*if*/ typeof this.additionalAuthorizationParameters === 'function' | ||
? this.additionalAuthorizationParameters() | ||
: /*if*/ typeof this.additionalAuthorizationParameters === 'object' | ||
? this.additionalAuthorizationParameters | ||
: {} | ||
); | ||
const popup = window.open(`${this.authorizationUri}?${OAuth2PopupFlow.encodeObjectToUri({ | ||
client_id: this.clientId, | ||
@@ -125,5 +355,5 @@ response_type: this.responseType, | ||
scope: this.scope, | ||
...this.additionalAuthorizationParameters, | ||
...additionalParams, | ||
})}`); | ||
if (!popup) { return false; } | ||
if (!popup) { return 'POPUP_FAILED'; } | ||
@@ -133,5 +363,10 @@ await this.authenticated(); | ||
popup.close(); | ||
return true; | ||
return 'SUCCESS'; | ||
} | ||
/** | ||
* A promise that does not resolve until `loggedIn()` is true. This uses the `pollingTime` | ||
* to wait until checking if `loggedIn()` is `true`. | ||
*/ | ||
async authenticated() { | ||
@@ -143,7 +378,8 @@ while (!this.loggedIn()) { | ||
/** | ||
* If the user is `loggedIn()`, the token will be returned immediate, else it will open a popup | ||
* and wait until the user is `loggedIn()` (i.e. a new token has been added). | ||
*/ | ||
async token() { | ||
if (!this.loggedIn()) { | ||
this.tryLoginPopup(); | ||
await this.authenticated(); | ||
} | ||
await this.authenticated(); | ||
const token = this._rawToken; | ||
@@ -156,7 +392,8 @@ if (!token) { | ||
/** | ||
* If the user is `loggedIn()`, the token payload will be returned immediate, else it will open a | ||
* popup and wait until the user is `loggedIn()` (i.e. a new token has been added). | ||
*/ | ||
async tokenPayload() { | ||
if (!this.loggedIn()) { | ||
this.tryLoginPopup(); | ||
await this.authenticated(); | ||
} | ||
await this.authenticated(); | ||
const payload = this._rawTokenPayload; | ||
@@ -169,6 +406,9 @@ if (!payload) { | ||
/** | ||
* wraps `JSON.parse` and return `undefined` if the parsing failed | ||
*/ | ||
static jsonParseOrUndefined<T = {}>(json: string) { | ||
try { | ||
return JSON.parse(json) as T; | ||
} catch { | ||
} catch (e) { | ||
return undefined; | ||
@@ -178,4 +418,10 @@ } | ||
/** | ||
* wraps `setTimeout` in a `Promise` that resolves to `'TIMER'` | ||
*/ | ||
static time(milliseconds: number) { | ||
return new Promise<'TIMER'>(resolve => setTimeout(() => resolve('TIMER'))); | ||
return new Promise<'TIMER'>(resolve => window.setTimeout( | ||
() => resolve('TIMER'), | ||
milliseconds | ||
)); | ||
} | ||
@@ -189,3 +435,3 @@ | ||
return decodeURIComponent(str); | ||
} catch { | ||
} catch (e) { | ||
return str; | ||
@@ -195,2 +441,7 @@ } | ||
/** | ||
* Encodes an object of strings to a URL | ||
* | ||
* `{one: 'two', buckle: 'shoes or something'}` ==> `one=two&buckle=shoes%20or%20something` | ||
*/ | ||
static encodeObjectToUri(obj: { [key: string]: string }) { | ||
@@ -205,2 +456,7 @@ return (Object | ||
/** | ||
* Decodes a URL string to an object of string | ||
* | ||
* `one=two&buckle=shoes%20or%20something` ==> `{one: 'two', buckle: 'shoes or something'}` | ||
*/ | ||
static decodeUriToObject(str: string) { | ||
@@ -207,0 +463,0 @@ return str.split('&').reduce((decoded, keyValuePair) => { |
@@ -5,4 +5,9 @@ { | ||
"module": "commonjs", | ||
"lib": ["dom", "es2015"], | ||
"strict": true | ||
"lib": [ | ||
"dom", | ||
"es2015" | ||
], | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true | ||
}, | ||
@@ -9,0 +14,0 @@ "exclude": [ |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
89920
1375
1
125
11
8
1