/www/lib/angular-jwt.js

https://gitlab.com/mshepherd/diris-app · JavaScript · 394 lines · 257 code · 59 blank · 78 comment · 37 complexity · 4d82d89da4e18617b3cd912eec46d639 MD5 · raw file

  1. (function() {
  2. // Create all modules and define dependencies to make sure they exist
  3. // and are loaded in the correct order to satisfy dependency injection
  4. // before all nested files are concatenated by Grunt
  5. // Modules
  6. angular.module('angular-jwt',
  7. [
  8. 'angular-jwt.options',
  9. 'angular-jwt.interceptor',
  10. 'angular-jwt.jwt',
  11. 'angular-jwt.authManager'
  12. ]);
  13. angular.module('angular-jwt.authManager', [])
  14. .provider('authManager', function () {
  15. this.$get = ["$rootScope", "$injector", "$location", "jwtHelper", "jwtInterceptor", "jwtOptions", function ($rootScope, $injector, $location, jwtHelper, jwtInterceptor, jwtOptions) {
  16. var config = jwtOptions.getConfig();
  17. function invokeToken(tokenGetter) {
  18. var token = null;
  19. if (Array.isArray(tokenGetter)) {
  20. token = $injector.invoke(tokenGetter, this, {options: null});
  21. } else {
  22. token = tokenGetter();
  23. }
  24. return token;
  25. }
  26. function invokeRedirector(redirector) {
  27. if (Array.isArray(redirector) || angular.isFunction(redirector)) {
  28. return $injector.invoke(redirector, config, {});
  29. } else {
  30. throw new Error('unauthenticatedRedirector must be a function');
  31. }
  32. }
  33. function isAuthenticated() {
  34. var token = invokeToken(config.tokenGetter);
  35. if (token) {
  36. return !jwtHelper.isTokenExpired(token);
  37. }
  38. }
  39. $rootScope.isAuthenticated = false;
  40. function authenticate() {
  41. $rootScope.isAuthenticated = true;
  42. }
  43. function unauthenticate() {
  44. $rootScope.isAuthenticated = false;
  45. }
  46. function checkAuthOnRefresh() {
  47. $rootScope.$on('$locationChangeStart', function () {
  48. var token = invokeToken(config.tokenGetter);
  49. if (token) {
  50. if (!jwtHelper.isTokenExpired(token)) {
  51. authenticate();
  52. } else {
  53. $rootScope.$broadcast('tokenHasExpired', token);
  54. }
  55. }
  56. });
  57. }
  58. function redirectWhenUnauthenticated() {
  59. $rootScope.$on('unauthenticated', function () {
  60. invokeRedirector(config.unauthenticatedRedirector);
  61. unauthenticate();
  62. });
  63. }
  64. function verifyRoute(event, next) {
  65. if (!next) {
  66. return false;
  67. }
  68. var routeData = (next.$$route) ? next.$$route : next.data;
  69. if (routeData && routeData.requiresLogin === true) {
  70. var token = invokeToken(config.tokenGetter);
  71. if (!token || jwtHelper.isTokenExpired(token)) {
  72. event.preventDefault();
  73. invokeRedirector(config.unauthenticatedRedirector);
  74. }
  75. }
  76. }
  77. var eventName = ($injector.has('$state')) ? '$stateChangeStart' : '$routeChangeStart';
  78. $rootScope.$on(eventName, verifyRoute);
  79. return {
  80. authenticate: authenticate,
  81. unauthenticate: unauthenticate,
  82. getToken: function(){ return invokeToken(config.tokenGetter); },
  83. redirect: function() { return invokeRedirector(config.unauthenticatedRedirector); },
  84. checkAuthOnRefresh: checkAuthOnRefresh,
  85. redirectWhenUnauthenticated: redirectWhenUnauthenticated,
  86. isAuthenticated: isAuthenticated
  87. }
  88. }]
  89. });
  90. angular.module('angular-jwt.interceptor', [])
  91. .provider('jwtInterceptor', function() {
  92. this.urlParam;
  93. this.authHeader;
  94. this.authPrefix;
  95. this.whiteListedDomains;
  96. this.tokenGetter;
  97. var config = this;
  98. this.$get = ["$q", "$injector", "$rootScope", "urlUtils", "jwtOptions", function($q, $injector, $rootScope, urlUtils, jwtOptions) {
  99. var options = angular.extend({}, jwtOptions.getConfig(), config);
  100. function isSafe (url) {
  101. if (!urlUtils.isSameOrigin(url) && !options.whiteListedDomains.length) {
  102. throw new Error('As of v0.1.0, requests to domains other than the application\'s origin must be white listed. Use jwtOptionsProvider.config({ whiteListedDomains: [<domain>] }); to whitelist.')
  103. }
  104. var hostname = urlUtils.urlResolve(url).hostname.toLowerCase();
  105. for (var i = 0; i < options.whiteListedDomains.length; i++) {
  106. var domain = options.whiteListedDomains[i];
  107. var regexp = domain instanceof RegExp ? domain : new RegExp(domain, 'i');
  108. if (hostname.match(regexp)) {
  109. return true;
  110. }
  111. }
  112. if (urlUtils.isSameOrigin(url)) {
  113. return true;
  114. }
  115. return false;
  116. }
  117. return {
  118. request: function (request) {
  119. if (request.skipAuthorization || !isSafe(request.url)) {
  120. return request;
  121. }
  122. if (options.urlParam) {
  123. request.params = request.params || {};
  124. // Already has the token in the url itself
  125. if (request.params[options.urlParam]) {
  126. return request;
  127. }
  128. } else {
  129. request.headers = request.headers || {};
  130. // Already has an Authorization header
  131. if (request.headers[options.authHeader]) {
  132. return request;
  133. }
  134. }
  135. var tokenPromise = $q.when($injector.invoke(options.tokenGetter, this, {
  136. options: request
  137. }));
  138. return tokenPromise.then(function(token) {
  139. if (token) {
  140. if (options.urlParam) {
  141. request.params[options.urlParam] = token;
  142. } else {
  143. request.headers[options.authHeader] = options.authPrefix + token;
  144. }
  145. }
  146. return request;
  147. });
  148. },
  149. responseError: function (response) {
  150. // handle the case where the user is not authenticated
  151. if (response.status === 401) {
  152. $rootScope.$broadcast('unauthenticated', response);
  153. }
  154. return $q.reject(response);
  155. }
  156. };
  157. }]
  158. });
  159. angular.module('angular-jwt.jwt', [])
  160. .service('jwtHelper', ["$window", function($window) {
  161. this.urlBase64Decode = function(str) {
  162. var output = str.replace(/-/g, '+').replace(/_/g, '/');
  163. switch (output.length % 4) {
  164. case 0: { break; }
  165. case 2: { output += '=='; break; }
  166. case 3: { output += '='; break; }
  167. default: {
  168. throw 'Illegal base64url string!';
  169. }
  170. }
  171. return $window.decodeURIComponent(escape($window.atob(output))); //polyfill https://github.com/davidchambers/Base64.js
  172. };
  173. this.decodeToken = function(token) {
  174. var parts = token.split('.');
  175. if (parts.length !== 3) {
  176. throw new Error('JWT must have 3 parts');
  177. }
  178. var decoded = this.urlBase64Decode(parts[1]);
  179. if (!decoded) {
  180. throw new Error('Cannot decode the token');
  181. }
  182. return angular.fromJson(decoded);
  183. };
  184. this.getTokenExpirationDate = function(token) {
  185. var decoded = this.decodeToken(token);
  186. if(typeof decoded.exp === "undefined") {
  187. return null;
  188. }
  189. var d = new Date(0); // The 0 here is the key, which sets the date to the epoch
  190. d.setUTCSeconds(decoded.exp);
  191. return d;
  192. };
  193. this.isTokenExpired = function(token, offsetSeconds) {
  194. var d = this.getTokenExpirationDate(token);
  195. offsetSeconds = offsetSeconds || 0;
  196. if (d === null) {
  197. return false;
  198. }
  199. // Token expired?
  200. return !(d.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000)));
  201. };
  202. }]);
  203. angular.module('angular-jwt.options', [])
  204. .provider('jwtOptions', function() {
  205. var globalConfig = {};
  206. this.config = function(value) {
  207. globalConfig = value;
  208. };
  209. this.$get = function() {
  210. var options = {
  211. urlParam: null,
  212. authHeader: 'Authorization',
  213. authPrefix: 'Bearer ',
  214. whiteListedDomains: [],
  215. tokenGetter: function() {
  216. return null;
  217. },
  218. loginPath: '/',
  219. unauthenticatedRedirectPath: '/',
  220. unauthenticatedRedirector: ['$location', function($location) {
  221. $location.path(this.unauthenticatedRedirectPath);
  222. }]
  223. };
  224. function JwtOptions() {
  225. var config = this.config = angular.extend({}, options, globalConfig);
  226. }
  227. JwtOptions.prototype.getConfig = function() {
  228. return this.config;
  229. };
  230. return new JwtOptions();
  231. }
  232. });
  233. /**
  234. * The content from this file was directly lifted from Angular. It is
  235. * unfortunately not a public API, so the best we can do is copy it.
  236. *
  237. * Angular References:
  238. * https://github.com/angular/angular.js/issues/3299
  239. * https://github.com/angular/angular.js/blob/d077966ff1ac18262f4615ff1a533db24d4432a7/src/ng/urlUtils.js
  240. */
  241. angular.module('angular-jwt.interceptor')
  242. .service('urlUtils', function () {
  243. // NOTE: The usage of window and document instead of $window and $document here is
  244. // deliberate. This service depends on the specific behavior of anchor nodes created by the
  245. // browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and
  246. // cause us to break tests. In addition, when the browser resolves a URL for XHR, it
  247. // doesn't know about mocked locations and resolves URLs to the real document - which is
  248. // exactly the behavior needed here. There is little value is mocking these out for this
  249. // service.
  250. var urlParsingNode = document.createElement("a");
  251. var originUrl = urlResolve(window.location.href);
  252. /**
  253. *
  254. * Implementation Notes for non-IE browsers
  255. * ----------------------------------------
  256. * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM,
  257. * results both in the normalizing and parsing of the URL. Normalizing means that a relative
  258. * URL will be resolved into an absolute URL in the context of the application document.
  259. * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
  260. * properties are all populated to reflect the normalized URL. This approach has wide
  261. * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See
  262. * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
  263. *
  264. * Implementation Notes for IE
  265. * ---------------------------
  266. * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other
  267. * browsers. However, the parsed components will not be set if the URL assigned did not specify
  268. * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We
  269. * work around that by performing the parsing in a 2nd step by taking a previously normalized
  270. * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the
  271. * properties such as protocol, hostname, port, etc.
  272. *
  273. * References:
  274. * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement
  275. * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
  276. * http://url.spec.whatwg.org/#urlutils
  277. * https://github.com/angular/angular.js/pull/2902
  278. * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
  279. *
  280. * @kind function
  281. * @param {string} url The URL to be parsed.
  282. * @description Normalizes and parses a URL.
  283. * @returns {object} Returns the normalized URL as a dictionary.
  284. *
  285. * | member name | Description |
  286. * |---------------|----------------|
  287. * | href | A normalized version of the provided URL if it was not an absolute URL |
  288. * | protocol | The protocol including the trailing colon |
  289. * | host | The host and port (if the port is non-default) of the normalizedUrl |
  290. * | search | The search params, minus the question mark |
  291. * | hash | The hash string, minus the hash symbol
  292. * | hostname | The hostname
  293. * | port | The port, without ":"
  294. * | pathname | The pathname, beginning with "/"
  295. *
  296. */
  297. function urlResolve(url) {
  298. var href = url;
  299. // Normalize before parse. Refer Implementation Notes on why this is
  300. // done in two steps on IE.
  301. urlParsingNode.setAttribute("href", href);
  302. href = urlParsingNode.href;
  303. urlParsingNode.setAttribute('href', href);
  304. // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
  305. return {
  306. href: urlParsingNode.href,
  307. protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
  308. host: urlParsingNode.host,
  309. search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
  310. hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
  311. hostname: urlParsingNode.hostname,
  312. port: urlParsingNode.port,
  313. pathname: (urlParsingNode.pathname.charAt(0) === '/')
  314. ? urlParsingNode.pathname
  315. : '/' + urlParsingNode.pathname
  316. };
  317. }
  318. /**
  319. * Parse a request URL and determine whether this is a same-origin request as the application document.
  320. *
  321. * @param {string|object} requestUrl The url of the request as a string that will be resolved
  322. * or a parsed URL object.
  323. * @returns {boolean} Whether the request is for the same origin as the application document.
  324. */
  325. function urlIsSameOrigin(requestUrl) {
  326. var parsed = (angular.isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
  327. return (parsed.protocol === originUrl.protocol &&
  328. parsed.host === originUrl.host);
  329. }
  330. return {
  331. urlResolve: urlResolve,
  332. isSameOrigin: urlIsSameOrigin
  333. };
  334. });
  335. }());