418 lines
14 KiB
JavaScript
Raw Normal View History

2020-05-19 11:43:42 +03:00
(function() {
// Create all modules and define dependencies to make sure they exist
// and are loaded in the correct order to satisfy dependency injection
// before all nested files are concatenated by Grunt
// Modules
angular.module('angular-jwt',
[
'angular-jwt.options',
'angular-jwt.interceptor',
'angular-jwt.jwt',
'angular-jwt.authManager'
]);
angular.module('angular-jwt.authManager', [])
.provider('authManager', function () {
this.$get = ["$rootScope", "$injector", "$location", "jwtHelper", "jwtInterceptor", "jwtOptions", function ($rootScope, $injector, $location, jwtHelper, jwtInterceptor, jwtOptions) {
var config = jwtOptions.getConfig();
function invokeToken(tokenGetter) {
var token = null;
if (Array.isArray(tokenGetter)) {
token = $injector.invoke(tokenGetter, this, {options: null});
} else {
token = tokenGetter();
}
return token;
}
function invokeRedirector(redirector) {
if (Array.isArray(redirector) || angular.isFunction(redirector)) {
return $injector.invoke(redirector, config, {});
} else {
throw new Error('unauthenticatedRedirector must be a function');
}
}
function isAuthenticated() {
var token = invokeToken(config.tokenGetter);
if (token) {
return !jwtHelper.isTokenExpired(token);
}
}
$rootScope.isAuthenticated = false;
function authenticate() {
$rootScope.isAuthenticated = true;
}
function unauthenticate() {
$rootScope.isAuthenticated = false;
}
function validateToken() {
var token = invokeToken(config.tokenGetter);
if (token) {
if (!jwtHelper.isTokenExpired(token)) {
authenticate();
} else {
$rootScope.$broadcast('tokenHasExpired', token);
}
}
}
function checkAuthOnRefresh() {
if ($injector.has('$transitions')) {
var $transitions = $injector.get('$transitions');
$transitions.onStart({}, validateToken);
} else {
$rootScope.$on('$locationChangeStart', validateToken);
}
}
function redirectWhenUnauthenticated() {
$rootScope.$on('unauthenticated', function () {
invokeRedirector(config.unauthenticatedRedirector);
unauthenticate();
});
}
function verifyRoute(event, next) {
if (!next) {
return false;
}
var routeData = (next.$$route) ? next.$$route : next.data;
if (routeData && routeData.requiresLogin === true && !isAuthenticated()) {
event.preventDefault();
invokeRedirector(config.unauthenticatedRedirector);
}
}
function verifyState(transition) {
var route = transition.to();
var $state = transition.router.stateService;
if (route && route.data && route.data.requiresLogin === true && !isAuthenticated()) {
return $state.target(config.loginPath);
}
}
if ($injector.has('$transitions')) {
var $transitions = $injector.get('$transitions');
$transitions.onStart({}, verifyState);
} else {
var eventName = ($injector.has('$state')) ? '$stateChangeStart' : '$routeChangeStart';
$rootScope.$on(eventName, verifyRoute);
}
return {
authenticate: authenticate,
unauthenticate: unauthenticate,
getToken: function(){ return invokeToken(config.tokenGetter); },
redirect: function() { return invokeRedirector(config.unauthenticatedRedirector); },
checkAuthOnRefresh: checkAuthOnRefresh,
redirectWhenUnauthenticated: redirectWhenUnauthenticated,
isAuthenticated: isAuthenticated
}
}]
});
angular.module('angular-jwt.interceptor', [])
.provider('jwtInterceptor', function() {
this.urlParam;
this.authHeader;
this.authPrefix;
this.whiteListedDomains;
this.tokenGetter;
var config = this;
this.$get = ["$q", "$injector", "$rootScope", "urlUtils", "jwtOptions", function($q, $injector, $rootScope, urlUtils, jwtOptions) {
var options = angular.extend({}, jwtOptions.getConfig(), config);
function isSafe (url) {
if (!urlUtils.isSameOrigin(url) && !options.whiteListedDomains.length) {
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.')
}
var hostname = urlUtils.urlResolve(url).hostname.toLowerCase();
for (var i = 0; i < options.whiteListedDomains.length; i++) {
var domain = options.whiteListedDomains[i];
if (domain instanceof RegExp) {
if (hostname.match(domain)) {
return true;
}
} else {
if (hostname === domain.toLowerCase()) {
return true;
}
}
}
if (urlUtils.isSameOrigin(url)) {
return true;
}
return false;
}
return {
request: function (request) {
if (request.skipAuthorization || !isSafe(request.url)) {
return request;
}
if (options.urlParam) {
request.params = request.params || {};
// Already has the token in the url itself
if (request.params[options.urlParam]) {
return request;
}
} else {
request.headers = request.headers || {};
// Already has an Authorization header
if (request.headers[options.authHeader]) {
return request;
}
}
var tokenPromise = $q.when($injector.invoke(options.tokenGetter, this, {
options: request
}));
return tokenPromise.then(function(token) {
if (token) {
if (options.urlParam) {
request.params[options.urlParam] = token;
} else {
request.headers[options.authHeader] = options.authPrefix + token;
}
}
return request;
});
},
responseError: function (response) {
// handle the case where the user is not authenticated
if (response !== undefined && response.status === 401) {
$rootScope.$broadcast('unauthenticated', response);
}
return $q.reject(response);
}
};
}]
});
angular.module('angular-jwt.jwt', [])
.service('jwtHelper', ["$window", function($window) {
this.urlBase64Decode = function(str) {
var output = str.replace(/-/g, '+').replace(/_/g, '/');
switch (output.length % 4) {
case 0: { break; }
case 2: { output += '=='; break; }
case 3: { output += '='; break; }
default: {
throw 'Illegal base64url string!';
}
}
return $window.decodeURIComponent(escape($window.atob(output))); //polyfill https://github.com/davidchambers/Base64.js
};
this.decodeToken = function(token) {
var parts = token.split('.');
if (parts.length !== 3) {
throw new Error('JWT must have 3 parts');
}
var decoded = this.urlBase64Decode(parts[1]);
if (!decoded) {
throw new Error('Cannot decode the token');
}
return angular.fromJson(decoded);
};
this.getTokenExpirationDate = function(token) {
var decoded = this.decodeToken(token);
if(typeof decoded.exp === "undefined") {
return null;
}
var d = new Date(0); // The 0 here is the key, which sets the date to the epoch
d.setUTCSeconds(decoded.exp);
return d;
};
this.isTokenExpired = function(token, offsetSeconds) {
var d = this.getTokenExpirationDate(token);
offsetSeconds = offsetSeconds || 0;
if (d === null) {
return false;
}
// Token expired?
return !(d.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000)));
};
}]);
angular.module('angular-jwt.options', [])
.provider('jwtOptions', function() {
var globalConfig = {};
this.config = function(value) {
globalConfig = value;
};
this.$get = function() {
var options = {
urlParam: null,
authHeader: 'Authorization',
authPrefix: 'Bearer ',
whiteListedDomains: [],
tokenGetter: function() {
return null;
},
loginPath: '/',
unauthenticatedRedirectPath: '/',
unauthenticatedRedirector: ['$location', function($location) {
$location.path(this.unauthenticatedRedirectPath);
}]
};
function JwtOptions() {
var config = this.config = angular.extend({}, options, globalConfig);
}
JwtOptions.prototype.getConfig = function() {
return this.config;
};
return new JwtOptions();
}
});
/**
* The content from this file was directly lifted from Angular. It is
* unfortunately not a public API, so the best we can do is copy it.
*
* Angular References:
* https://github.com/angular/angular.js/issues/3299
* https://github.com/angular/angular.js/blob/d077966ff1ac18262f4615ff1a533db24d4432a7/src/ng/urlUtils.js
*/
angular.module('angular-jwt.interceptor')
.service('urlUtils', function () {
// NOTE: The usage of window and document instead of $window and $document here is
// deliberate. This service depends on the specific behavior of anchor nodes created by the
// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and
// cause us to break tests. In addition, when the browser resolves a URL for XHR, it
// doesn't know about mocked locations and resolves URLs to the real document - which is
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
var urlParsingNode = document.createElement("a");
var originUrl = urlResolve(window.location.href);
/**
*
* Implementation Notes for non-IE browsers
* ----------------------------------------
* Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM,
* results both in the normalizing and parsing of the URL. Normalizing means that a relative
* URL will be resolved into an absolute URL in the context of the application document.
* Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
* properties are all populated to reflect the normalized URL. This approach has wide
* compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
*
* Implementation Notes for IE
* ---------------------------
* IE <= 10 normalizes the URL when assigned to the anchor node similar to the other
* browsers. However, the parsed components will not be set if the URL assigned did not specify
* them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We
* work around that by performing the parsing in a 2nd step by taking a previously normalized
* URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the
* properties such as protocol, hostname, port, etc.
*
* References:
* http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
* http://url.spec.whatwg.org/#urlutils
* https://github.com/angular/angular.js/pull/2902
* http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
*
* @kind function
* @param {string} url The URL to be parsed.
* @description Normalizes and parses a URL.
* @returns {object} Returns the normalized URL as a dictionary.
*
* | member name | Description |
* |---------------|----------------|
* | href | A normalized version of the provided URL if it was not an absolute URL |
* | protocol | The protocol including the trailing colon |
* | host | The host and port (if the port is non-default) of the normalizedUrl |
* | search | The search params, minus the question mark |
* | hash | The hash string, minus the hash symbol
* | hostname | The hostname
* | port | The port, without ":"
* | pathname | The pathname, beginning with "/"
*
*/
function urlResolve(url) {
var href = url;
// Normalize before parse. Refer Implementation Notes on why this is
// done in two steps on IE.
urlParsingNode.setAttribute("href", href);
href = urlParsingNode.href;
urlParsingNode.setAttribute('href', href);
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
return {
href: urlParsingNode.href,
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
host: urlParsingNode.host,
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
hostname: urlParsingNode.hostname,
port: urlParsingNode.port,
pathname: (urlParsingNode.pathname.charAt(0) === '/')
? urlParsingNode.pathname
: '/' + urlParsingNode.pathname
};
}
/**
* Parse a request URL and determine whether this is a same-origin request as the application document.
*
* @param {string|object} requestUrl The url of the request as a string that will be resolved
* or a parsed URL object.
* @returns {boolean} Whether the request is for the same origin as the application document.
*/
function urlIsSameOrigin(requestUrl) {
var parsed = (angular.isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
return (parsed.protocol === originUrl.protocol &&
parsed.host === originUrl.host);
}
return {
urlResolve: urlResolve,
isSameOrigin: urlIsSameOrigin
};
});
}());