613 lines
14 KiB
JavaScript
613 lines
14 KiB
JavaScript
(function(exp, $) {
|
||
|
||
var
|
||
config = {},
|
||
default_lifetime = 3600,
|
||
options = {
|
||
"debug": false
|
||
},
|
||
|
||
api_redirect,
|
||
Api_default_storage,
|
||
api_storage,
|
||
|
||
internalStates = [];
|
||
|
||
/*
|
||
* ------ SECTION: Utilities
|
||
*/
|
||
|
||
/*
|
||
* Returns a random string used for state
|
||
*/
|
||
var uuid = function() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* A log wrapper, that only logs if logging is turned on in the config
|
||
* @param {string} msg Log message
|
||
*/
|
||
var log = function(msg) {
|
||
if (!options.debug) return;
|
||
if (!console) return;
|
||
if (!console.log) return;
|
||
|
||
// console.log("LOG(), Arguments", arguments, msg)
|
||
if (arguments.length > 1) {
|
||
console.log(arguments);
|
||
} else {
|
||
console.log(msg);
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Set the global options.
|
||
*/
|
||
var setOptions = function(opts) {
|
||
if (!opts) return;
|
||
for(var k in opts) {
|
||
if (opts.hasOwnProperty(k)) {
|
||
options[k] = opts[k];
|
||
}
|
||
}
|
||
log("Options is set to ", options);
|
||
}
|
||
|
||
|
||
/*
|
||
* Takes an URL as input and a params object.
|
||
* Each property in the params is added to the url as query string parameters
|
||
*/
|
||
var encodeURL = function(url, params) {
|
||
var res = url;
|
||
var k, i = 0;
|
||
var firstSeparator = (url.indexOf("?") === -1) ? '?' : '&';
|
||
for(k in params) {
|
||
res += (i++ === 0 ? firstSeparator : '&') + encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
|
||
|
||
/*
|
||
* Redirects the user to a specific URL
|
||
*/
|
||
api_redirect = function(url) {
|
||
window.location = url;
|
||
};
|
||
|
||
Api_default_storage = function() {
|
||
log("Constructor");
|
||
};
|
||
|
||
/**
|
||
saveState stores an object with an Identifier.
|
||
TODO: Ensure that both localstorage and JSON encoding has fallbacks for ancient browsers.
|
||
In the state object, we put the request object, plus these parameters:
|
||
* restoreHash
|
||
* providerID
|
||
* scopes
|
||
|
||
*/
|
||
Api_default_storage.prototype.saveState = function (state, obj) {
|
||
localStorage.setItem("state-" + state, JSON.stringify(obj));
|
||
}
|
||
|
||
|
||
/**
|
||
* getStage() returns the state object, but also removes it.
|
||
* @type {Object}
|
||
*/
|
||
Api_default_storage.prototype.getState = function(state) {
|
||
// log("getState (" + state+ ")");
|
||
var obj = JSON.parse(localStorage.getItem("state-" + state));
|
||
localStorage.removeItem("state-" + state)
|
||
return obj;
|
||
};
|
||
|
||
|
||
/*
|
||
* Checks if a token, has includes a specific scope.
|
||
* If token has no scope at all, false is returned.
|
||
*/
|
||
Api_default_storage.prototype.hasScope = function(token, scope) {
|
||
var i;
|
||
if (!token.scopes) return false;
|
||
for(i = 0; i < token.scopes.length; i++) {
|
||
if (token.scopes[i] === scope) return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/*
|
||
* Takes an array of tokens, and removes the ones that
|
||
* are expired, and the ones that do not meet a scopes requirement.
|
||
*/
|
||
Api_default_storage.prototype.filterTokens = function(tokens, scopes) {
|
||
var i, j,
|
||
result = [],
|
||
now = epoch(),
|
||
usethis;
|
||
|
||
if (!scopes) scopes = [];
|
||
|
||
for(i = 0; i < tokens.length; i++) {
|
||
usethis = true;
|
||
|
||
// Filter out expired tokens. Tokens that is expired in 1 second from now.
|
||
if (tokens[i].expires && tokens[i].expires < (now+1)) usethis = false;
|
||
|
||
// Filter out this token if not all scope requirements are met
|
||
for(j = 0; j < scopes.length; j++) {
|
||
if (!api_storage.hasScope(tokens[i], scopes[j])) usethis = false;
|
||
}
|
||
|
||
if (usethis) result.push(tokens[i]);
|
||
}
|
||
return result;
|
||
};
|
||
|
||
|
||
/*
|
||
* saveTokens() stores a list of tokens for a provider.
|
||
|
||
Usually the tokens stored are a plain Access token plus:
|
||
* expires : time that the token expires
|
||
* providerID: the provider of the access token?
|
||
* scopes: an array with the scopes (not string)
|
||
*/
|
||
Api_default_storage.prototype.saveTokens = function(provider, tokens) {
|
||
// log("Save Tokens (" + provider+ ")");
|
||
localStorage.setItem("tokens-" + provider, JSON.stringify(tokens));
|
||
};
|
||
|
||
Api_default_storage.prototype.getTokens = function(provider) {
|
||
// log("Get Tokens (" + provider+ ")");
|
||
var tokens = JSON.parse(localStorage.getItem("tokens-" + provider));
|
||
if (!tokens) tokens = [];
|
||
|
||
log("Token received", tokens)
|
||
return tokens;
|
||
};
|
||
Api_default_storage.prototype.wipeTokens = function(provider) {
|
||
localStorage.removeItem("tokens-" + provider);
|
||
};
|
||
/*
|
||
* Save a single token for a provider.
|
||
* This also cleans up expired tokens for the same provider.
|
||
*/
|
||
Api_default_storage.prototype.saveToken = function(provider, token) {
|
||
var tokens = this.getTokens(provider);
|
||
tokens = api_storage.filterTokens(tokens);
|
||
tokens.push(token);
|
||
this.saveTokens(provider, tokens);
|
||
};
|
||
|
||
/*
|
||
* Get a token if exists for a provider with a set of scopes.
|
||
* The scopes parameter is OPTIONAL.
|
||
*/
|
||
Api_default_storage.prototype.getToken = function(provider, scopes) {
|
||
var tokens = this.getTokens(provider);
|
||
tokens = api_storage.filterTokens(tokens, scopes);
|
||
if (tokens.length < 1) return null;
|
||
return tokens[0];
|
||
};
|
||
|
||
api_storage = new Api_default_storage();
|
||
|
||
|
||
|
||
/*
|
||
* ------ SECTION: Utilities
|
||
*/
|
||
|
||
/*
|
||
* Returns a random string used for state
|
||
*/
|
||
var uuid = function() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
/*
|
||
* Takes an URL as input and a params object.
|
||
* Each property in the params is added to the url as query string parameters
|
||
*/
|
||
var encodeURL = function(url, params) {
|
||
var res = url;
|
||
var k, i = 0;
|
||
for(k in params) {
|
||
res += (i++ === 0 ? '?' : '&') + encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
/*
|
||
* Returns epoch, seconds since 1970.
|
||
* Used for calculation of expire times.
|
||
*/
|
||
var epoch = function() {
|
||
return Math.round(new Date().getTime()/1000.0);
|
||
}
|
||
|
||
|
||
|
||
var parseQueryString = function (qs) {
|
||
var e,
|
||
a = /\+/g, // Regex for replacing addition symbol with a space
|
||
r = /([^&;=]+)=?([^&;]*)/g,
|
||
d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
|
||
q = qs,
|
||
urlParams = {};
|
||
|
||
while (e = r.exec(q))
|
||
urlParams[d(e[1])] = d(e[2]);
|
||
|
||
return urlParams;
|
||
}
|
||
/*
|
||
* ------ / SECTION: Utilities
|
||
*/
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Check if the hash contains an access token.
|
||
* And if it do, extract the state, compare with
|
||
* config, and store the access token for later use.
|
||
*
|
||
* The url parameter is optional. Used with phonegap and
|
||
* childbrowser when the jso context is not receiving the response,
|
||
* instead the response is received on a child browser.
|
||
*/
|
||
exp.jso_checkfortoken = function(providerID, url, callback) {
|
||
var
|
||
atoken,
|
||
h = window.location.hash,
|
||
now = epoch(),
|
||
state,
|
||
co;
|
||
|
||
log("jso_checkfortoken(" + providerID + ")");
|
||
|
||
// If a url is provided
|
||
if (url) {
|
||
// log('Hah, I got the url and it ' + url);
|
||
if(url.indexOf('#') === -1) return;
|
||
h = url.substring(url.indexOf('#'));
|
||
// log('Hah, I got the hash and it is ' + h);
|
||
}
|
||
|
||
/*
|
||
* Start with checking if there is a token in the hash
|
||
*/
|
||
if (h.length < 2) return;
|
||
if (h.indexOf("access_token") === -1) return;
|
||
h = h.substring(1);
|
||
atoken = parseQueryString(h);
|
||
|
||
if (atoken.state) {
|
||
state = api_storage.getState(atoken.state);
|
||
} else {
|
||
if (!providerID) {throw "Could not get [state] and no default providerid is provided.";}
|
||
state = {providerID: providerID};
|
||
}
|
||
|
||
|
||
if (!state) throw "Could not retrieve state";
|
||
if (!state.providerID) throw "Could not get providerid from state";
|
||
if (!config[state.providerID]) throw "Could not retrieve config for this provider.";
|
||
co = config[state.providerID];
|
||
|
||
/**
|
||
* If state was not provided, and default provider contains a scope parameter
|
||
* we assume this is the one requested...
|
||
*/
|
||
if (!atoken.state && co.scope) {
|
||
state.scopes = co.scope;
|
||
log("Setting state: ", state);
|
||
}
|
||
log("Checking atoken ", atoken, " and co ", co);
|
||
|
||
/*
|
||
* Decide when this token should expire.
|
||
* Priority fallback:
|
||
* 1. Access token expires_in
|
||
* 2. Life time in config (may be false = permanent...)
|
||
* 3. Specific permanent scope.
|
||
* 4. Default library lifetime:
|
||
*/
|
||
if (atoken["expires_in"]) {
|
||
atoken["expires"] = now + parseInt(atoken["expires_in"], 10);
|
||
} else if (co["default_lifetime"] === false) {
|
||
// Token is permanent.
|
||
} else if (co["default_lifetime"]) {
|
||
atoken["expires"] = now + co["default_lifetime"];
|
||
} else if (co["permanent_scope"]) {
|
||
if (!api_storage.hasScope(atoken, co["permanent_scope"])) {
|
||
atoken["expires"] = now + default_lifetime;
|
||
}
|
||
} else {
|
||
atoken["expires"] = now + default_lifetime;
|
||
}
|
||
|
||
/*
|
||
* Handle scopes for this token
|
||
*/
|
||
if (atoken["scope"]) {
|
||
atoken["scopes"] = atoken["scope"].split(" ");
|
||
} else if (state["scopes"]) {
|
||
atoken["scopes"] = state["scopes"];
|
||
}
|
||
|
||
|
||
|
||
api_storage.saveToken(state.providerID, atoken);
|
||
|
||
if (state.restoreHash) {
|
||
window.location.hash = state.restoreHash;
|
||
} else {
|
||
window.location.hash = '';
|
||
}
|
||
|
||
|
||
log(atoken);
|
||
|
||
if (internalStates[atoken.state] && typeof internalStates[atoken.state] === 'function') {
|
||
// log("InternalState is set, calling it now!");
|
||
internalStates[atoken.state]();
|
||
delete internalStates[atoken.state];
|
||
}
|
||
|
||
|
||
if (typeof callback === 'function') {
|
||
callback();
|
||
}
|
||
|
||
// log(atoken);
|
||
|
||
}
|
||
|
||
/*
|
||
* A config object contains:
|
||
*/
|
||
var jso_authrequest = function(providerid, scopes, callback) {
|
||
|
||
var
|
||
state,
|
||
request,
|
||
authurl,
|
||
co;
|
||
|
||
if (!config[providerid]) throw "Could not find configuration for provider " + providerid;
|
||
co = config[providerid];
|
||
|
||
log("About to send an authorization request to [" + providerid + "]. Config:")
|
||
log(co);
|
||
|
||
state = uuid();
|
||
request = {
|
||
"response_type": "token"
|
||
};
|
||
request.state = state;
|
||
|
||
if (callback && typeof callback === 'function') {
|
||
internalStates[state] = callback;
|
||
}
|
||
|
||
|
||
if (co["redirect_uri"]) {
|
||
request["redirect_uri"] = co["redirect_uri"];
|
||
}
|
||
if (co["client_id"]) {
|
||
request["client_id"] = co["client_id"];
|
||
}
|
||
if (scopes) {
|
||
request["scope"] = scopes.join(" ");
|
||
}
|
||
|
||
authurl = encodeURL(co.authorization, request);
|
||
|
||
// We'd like to cache the hash for not loosing Application state.
|
||
// With the implciit grant flow, the hash will be replaced with the access
|
||
// token when we return after authorization.
|
||
if (window.location.hash) {
|
||
request["restoreHash"] = window.location.hash;
|
||
}
|
||
request["providerID"] = providerid;
|
||
if (scopes) {
|
||
request["scopes"] = scopes;
|
||
}
|
||
|
||
|
||
log("Saving state [" + state+ "]");
|
||
log(JSON.parse(JSON.stringify(request)));
|
||
|
||
api_storage.saveState(state, request);
|
||
api_redirect(authurl);
|
||
|
||
};
|
||
|
||
exp.jso_ensureTokens = function (ensure) {
|
||
var providerid, scopes, token;
|
||
for(providerid in ensure) {
|
||
scopes = undefined;
|
||
if (ensure[providerid]) scopes = ensure[providerid];
|
||
token = api_storage.getToken(providerid, scopes);
|
||
|
||
log("Ensure token for provider [" + providerid + "] ");
|
||
log(token);
|
||
|
||
if (token === null) {
|
||
jso_authrequest(providerid, scopes);
|
||
}
|
||
}
|
||
|
||
|
||
return true;
|
||
}
|
||
|
||
exp.jso_findDefaultEntry = function(c) {
|
||
var
|
||
k,
|
||
i = 0;
|
||
|
||
if (!c) return;
|
||
log("c", c);
|
||
for(k in c) {
|
||
i++;
|
||
if (c[k].isDefault && c[k].isDefault === true) {
|
||
return k;
|
||
}
|
||
}
|
||
if (i === 1) return k;
|
||
};
|
||
|
||
exp.jso_configure = function(c, opts) {
|
||
config = c;
|
||
setOptions(opts);
|
||
try {
|
||
|
||
var def = jso_findDefaultEntry(c);
|
||
log("jso_configure() about to check for token for this entry", def);
|
||
exp.jso_checkfortoken(def);
|
||
|
||
} catch(e) {
|
||
log("Error when retrieving token from hash: " + e);
|
||
window.location.hash = "";
|
||
}
|
||
|
||
}
|
||
|
||
exp.jso_dump = function() {
|
||
var key;
|
||
for(key in config) {
|
||
|
||
log("=====> Processing provider [" + key + "]");
|
||
log("=] Config");
|
||
log(config[key]);
|
||
log("=] Tokens")
|
||
log(api_storage.getTokens(key));
|
||
|
||
}
|
||
}
|
||
|
||
exp.jso_wipe = function() {
|
||
var key;
|
||
log("jso_wipe()");
|
||
for(key in config) {
|
||
log("Wipping tokens for " + key);
|
||
api_storage.wipeTokens(key);
|
||
}
|
||
}
|
||
|
||
exp.jso_getToken = function(providerid, scopes) {
|
||
var token = api_storage.getToken(providerid, scopes);
|
||
if (!token) return null;
|
||
if (!token["access_token"]) return null;
|
||
return token["access_token"];
|
||
}
|
||
|
||
|
||
|
||
exp.jso_registerRedirectHandler = function(callback) {
|
||
api_redirect = callback;
|
||
};
|
||
|
||
exp.jso_registerStorageHandler = function(object) {
|
||
api_storage = object;
|
||
};
|
||
|
||
|
||
/*
|
||
* From now on, we only perform tasks that require jQuery.
|
||
* Like adding the $.oajax function.
|
||
*/
|
||
if (typeof $ === 'undefined') return;
|
||
|
||
$.oajax = function(settings) {
|
||
var
|
||
allowia,
|
||
scopes,
|
||
token,
|
||
providerid,
|
||
co;
|
||
|
||
providerid = settings.jso_provider;
|
||
allowia = settings.jso_allowia || false;
|
||
scopes = settings.jso_scopes;
|
||
token = api_storage.getToken(providerid, scopes);
|
||
co = config[providerid];
|
||
|
||
// var successOverridden = settings.success;
|
||
// settings.success = function(response) {
|
||
// }
|
||
|
||
var errorOverridden = settings.error || null;
|
||
|
||
var performAjax = function() {
|
||
// log("Perform ajax!");
|
||
|
||
if (!token) throw "Could not perform AJAX call because no valid tokens was found.";
|
||
|
||
if (co["presenttoken"] && co["presenttoken"] === "qs") {
|
||
// settings.url += ((h.indexOf("?") === -1) ? '?' : '&') + "access_token=" + encodeURIComponent(token["access_token"]);
|
||
if (!settings.data) settings.data = {};
|
||
settings.data["access_token"] = token["access_token"];
|
||
} else {
|
||
if (!settings.headers) settings.headers = {};
|
||
settings.headers["Authorization"] = "Bearer " + token["access_token"];
|
||
}
|
||
$.ajax(settings);
|
||
};
|
||
|
||
settings.error = function(jqXHR, textStatus, errorThrown) {
|
||
log('error(jqXHR, textStatus, errorThrown)');
|
||
log(jqXHR);
|
||
log(textStatus);
|
||
log(errorThrown);
|
||
|
||
if (jqXHR.status === 401) {
|
||
|
||
log("Token expired. About to delete this token");
|
||
log(token);
|
||
api_storage.wipeTokens(providerid);
|
||
|
||
}
|
||
if (errorOverridden && typeof errorOverridden === 'function') {
|
||
errorOverridden(jqXHR, textStatus, errorThrown);
|
||
}
|
||
}
|
||
|
||
|
||
if (!token) {
|
||
if (allowia) {
|
||
log("Perform authrequest");
|
||
jso_authrequest(providerid, scopes, function() {
|
||
token = api_storage.getToken(providerid, scopes);
|
||
performAjax();
|
||
});
|
||
return;
|
||
} else {
|
||
throw "Could not perform AJAX call because no valid tokens was found.";
|
||
}
|
||
}
|
||
|
||
|
||
performAjax();
|
||
};
|
||
|
||
|
||
})(window, window.jQuery);
|