Merge branch 'v1' of git.daplie.com:Daplie/oauth3.js into v1
This commit is contained in:
commit
4657fcdb12
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #ffcccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Redirecting...
|
||||||
|
|
||||||
|
<!-- TODO permanently cache with appcache (or service worker?) -->
|
||||||
|
<!-- TODO slim this all down to a single file -->
|
||||||
|
<script src="/assets/org.oauth3/oauth3.core.js"></script>
|
||||||
|
<script src="callback.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,66 @@
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var loc = window.location;
|
||||||
|
var loginWinObj = window.OAUTH3_CORE.queryparse(loc.hash || loc.search);
|
||||||
|
var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/callback.html]";
|
||||||
|
|
||||||
|
if (loginWinObj.debug) {
|
||||||
|
console.warn(prefix, "DEBUG MODE ENABLED. Automatic redirects disabled.");
|
||||||
|
}
|
||||||
|
// '--oauth3-callback-' prefix exist for security so that an attacker can't social engineer execution an arbitrary function
|
||||||
|
// TODO finalize name of '--oauth3-callback-', this will be a defacto standard
|
||||||
|
// TODO maybe call it 'self-xss-' or 'hack-my-account-' to discourage people from doing dumb things?
|
||||||
|
var callbackName = '--oauth3-callback-' + loginWinObj.state;
|
||||||
|
|
||||||
|
console.log(prefix, loc.href);
|
||||||
|
console.log('Parsed URL Obj: ', loginWinObj);
|
||||||
|
console.log('callbackName: ', callbackName);
|
||||||
|
|
||||||
|
window.oauth3complete = function () {
|
||||||
|
// The hacks that used to be necessary for this on iOS should no longer be necessary in iOS 9+
|
||||||
|
// see https://bugs.chromium.org/p/chromium/issues/detail?id=136610 and https://crbug.com/423444
|
||||||
|
// TODO Should we still create an abstraction for older versions?
|
||||||
|
if (window.parent) {
|
||||||
|
// iframe
|
||||||
|
try {
|
||||||
|
window.parent[callbackName](loginWinObj);
|
||||||
|
return;
|
||||||
|
} catch(e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.opener) {
|
||||||
|
try {
|
||||||
|
window.opener[callbackName](loginWinObj);
|
||||||
|
return;
|
||||||
|
} catch(e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("neither window.parent nor window.opener existed to complete callback");
|
||||||
|
|
||||||
|
/*
|
||||||
|
// the caller should close (or signal to close) the window
|
||||||
|
try {
|
||||||
|
window.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error: ', err);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!loginWinObj.debug) {
|
||||||
|
window.oauth3complete();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.body.innerHTML = window.location.hostname + window.location.pathname
|
||||||
|
+ '<br/><br/>You\'ve passed the \'debug\' parameter so we\'re pausing'
|
||||||
|
+ ' to let you look at logs or whatever it is that you intended to do.'
|
||||||
|
+ '<br/><br/>Continue with callback: <a href="javascript:window.oauth3complete()">javascript:window.oauth3complete()</' + 'a>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}());
|
|
@ -0,0 +1,330 @@
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
console.log('[DAPLIE oauth3 directives.js]');
|
||||||
|
console.log(window.location);
|
||||||
|
|
||||||
|
var iter = 0;
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
|
||||||
|
var rpc = {};
|
||||||
|
//var myself = location.protocol + '//' + location.host + location.pathname;
|
||||||
|
var incoming;
|
||||||
|
var forwarding = {};
|
||||||
|
var err;
|
||||||
|
var browserState;
|
||||||
|
var browserCallback;
|
||||||
|
var action;
|
||||||
|
|
||||||
|
function parseParams() {
|
||||||
|
var params = {};
|
||||||
|
|
||||||
|
function parseParamsString(str) {
|
||||||
|
str.substr(1).split('&').filter(function (el) { return el; }).forEach(function (pair) {
|
||||||
|
pair = pair.split('=');
|
||||||
|
var key = decodeURIComponent(pair[0]);
|
||||||
|
var val = decodeURIComponent(pair[1]);
|
||||||
|
|
||||||
|
if (params[key]) {
|
||||||
|
console.warn("overwriting key '" + key + "' '" + params[key] + "'");
|
||||||
|
}
|
||||||
|
params[key] = val;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parseParamsString(window.location.search);
|
||||||
|
// handle cases where hash is treated like it's own href
|
||||||
|
// TODO /#/?search=blah
|
||||||
|
parseParamsString(window.location.hash);
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function querystringify(params) {
|
||||||
|
var arr = [];
|
||||||
|
|
||||||
|
Object.keys(params).forEach(function (k) {
|
||||||
|
arr.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));
|
||||||
|
});
|
||||||
|
|
||||||
|
return arr.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
function phoneAway(/*redirectURi, params*/) {
|
||||||
|
// TODO test for ? / #
|
||||||
|
window.location.href = incoming.redirect_uri + '#' + querystringify(forwarding);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lintAndSetRedirectable(browserState, params) {
|
||||||
|
if (!params.redirect_uri) {
|
||||||
|
window.alert('redirect_uri not defined');
|
||||||
|
err = new Error('redirect_uri not defined');
|
||||||
|
console.error(err.message);
|
||||||
|
console.warn(err.stack);
|
||||||
|
params.redirect_uri = document.referer;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!browserState) {
|
||||||
|
forwarding.error = "E_NO_BROWSER_STATE";
|
||||||
|
forwarding.error_description = "you must specify a state parameter";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('oauth3.states.' + browserState, JSON.stringify(params));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectCallback() {
|
||||||
|
var redirect_uri = incoming.redirect_uri;
|
||||||
|
forwarding.callback = browserState;
|
||||||
|
forwarding.action = 'close';
|
||||||
|
|
||||||
|
var url = redirect_uri + '#' + querystringify(forwarding);
|
||||||
|
|
||||||
|
console.log('[debug] redirect_uri + params:', url);
|
||||||
|
window.location.href = url;
|
||||||
|
setTimeout(function () {
|
||||||
|
if (iter >= 3) {
|
||||||
|
console.log("dancing way too much... stopping now");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
iter += 1;
|
||||||
|
console.log("I'm dancing by myse-e-elf");
|
||||||
|
// in case I'm redirecting to myself
|
||||||
|
main();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc = {};
|
||||||
|
|
||||||
|
// Act as a provider and log the user out
|
||||||
|
rpc.logout = function (browserState, incoming) {
|
||||||
|
var url;
|
||||||
|
if (!lintAndSetRedirectable(browserState, incoming)) {
|
||||||
|
// TODO fail
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('oauth3.states.' + browserState, JSON.stringify(incoming));
|
||||||
|
url = '/#/logout/' + browserState;
|
||||||
|
|
||||||
|
// TODO specify specific account or all?
|
||||||
|
window.location.href = url;
|
||||||
|
setTimeout(function () {
|
||||||
|
// in case I'm redirecting to myself
|
||||||
|
main();
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act as a provider and inform the consumer the logout is complete
|
||||||
|
rpc.logout_callback = function (browserState/*, incoming*/) {
|
||||||
|
// TODO pass redirect_uri and state through here so we can avoid localStorage
|
||||||
|
var forwarding = {};
|
||||||
|
var originalRequest;
|
||||||
|
|
||||||
|
if (!browserState) {
|
||||||
|
forwarding.error = "E_NO_BROWSER_STATE";
|
||||||
|
forwarding.error_description = "you must specify a state parameter";
|
||||||
|
if (incoming.redirect_uri) {
|
||||||
|
phoneAway(incoming.redirect_uri, forwarding);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest = JSON.parse(localStorage.getItem('oauth3.states.' + browserState));
|
||||||
|
forwarding.action = 'close';
|
||||||
|
forwarding.state = browserState;
|
||||||
|
//phoneAway(originalRequest.redirect_uri, forwarding);
|
||||||
|
window.location.href = originalRequest.redirect_uri + '#' + querystringify(forwarding);
|
||||||
|
};
|
||||||
|
|
||||||
|
rpc.directives = function (browserState, incoming) {
|
||||||
|
if (!lintAndSetRedirectable(browserState, incoming)) {
|
||||||
|
phoneAway();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedAt = new Date(localStorage.getItem('oauth3.directives.updated_at')).valueOf();
|
||||||
|
var fresh = (Date.now() - updatedAt) < (24 * 60 * 60 * 1000);
|
||||||
|
var directives = localStorage.getItem('oauth3.directives');
|
||||||
|
var redirected = false;
|
||||||
|
|
||||||
|
function redirectIf() {
|
||||||
|
if (redirected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirected = true;
|
||||||
|
redirectCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directives) {
|
||||||
|
forwarding.directives = directives;
|
||||||
|
redirectIf();
|
||||||
|
if (fresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open('GET', '.well-known/oauth3.json', true);
|
||||||
|
req.addEventListener('readystatechange', function () {
|
||||||
|
if (4 !== req.readyState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (200 !== req.status) {
|
||||||
|
forwarding.error = "E_STATUS_" + req.status;
|
||||||
|
forwarding.error_description = "expected 200 OK json or text response for oauth3.json but got '" + req.status + "'";
|
||||||
|
redirectIf();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
directives = btoa(JSON.stringify(JSON.parse(req.responseText)));
|
||||||
|
forwarding.directives = directives;
|
||||||
|
forwarding.callback = browserState;
|
||||||
|
localStorage.setItem('oauth3.directives', directives);
|
||||||
|
localStorage.setItem('oauth3.directives.updated_at', new Date().toISOString());
|
||||||
|
} catch(e) {
|
||||||
|
forwarding.error = "E_PARSE_JSON";
|
||||||
|
forwarding.error_description = e.message;
|
||||||
|
console.error(forwarding.error);
|
||||||
|
console.error(forwarding.error_description);
|
||||||
|
console.error(req.responseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectIf();
|
||||||
|
});
|
||||||
|
req.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
// the provider is contacting me
|
||||||
|
rpc.close = function (browserState, incoming) {
|
||||||
|
incoming.callback = browserState;
|
||||||
|
catchAll();
|
||||||
|
};
|
||||||
|
// the provider is contacting me
|
||||||
|
rpc.redirect = function (/*browserState, incoming*/) {
|
||||||
|
catchAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
function catchAll() {
|
||||||
|
function phoneHome() {
|
||||||
|
if (browserCallback === 'completeLogin') {
|
||||||
|
// Deprecated
|
||||||
|
console.log('[deprecated] callback completeLogin');
|
||||||
|
(window.opener||window.parent).completeLogin(null, null, incoming);
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG] I would be closed by my parent now');
|
||||||
|
console.log('__oauth3_' + browserCallback);
|
||||||
|
console.log(window.opener && window.opener['__oauth3_' + browserCallback]);
|
||||||
|
console.log(window.parent && window.parent['__oauth3_' + browserCallback]);
|
||||||
|
console.log(incoming);
|
||||||
|
(window.opener||window.parent)['__oauth3_' + browserCallback](incoming);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(incoming.browser_state || incoming.state)) {
|
||||||
|
window.alert("callback URLs should include 'browser_state' (authorization code)"
|
||||||
|
+ " or 'state' (implicit grant))");
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
// opener is for popup window, new tab
|
||||||
|
// parent is for iframe
|
||||||
|
phoneHome();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// iOS Webview (namely Chrome) workaround
|
||||||
|
setTimeout(function () {
|
||||||
|
console.log('I would close now');
|
||||||
|
// XXX OAUTH3 DEBUG FRAME XXX // make this easy to find
|
||||||
|
window.open('', '_self', '');
|
||||||
|
window.close();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
var i;
|
||||||
|
var len = localStorage.length;
|
||||||
|
var key;
|
||||||
|
var json;
|
||||||
|
var fresh;
|
||||||
|
|
||||||
|
for (i = 0; i < len; i += 1) {
|
||||||
|
key = localStorage.key(i);
|
||||||
|
// TODO check updatedAt
|
||||||
|
if (/^oauth3\./.test(key)) {
|
||||||
|
try {
|
||||||
|
json = localStorage.getItem(key);
|
||||||
|
if (json) {
|
||||||
|
json = JSON.parse(json);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
json = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fresh = json && (Date.now() - json.updatedAt < (5 * 60 * 1000));
|
||||||
|
|
||||||
|
if (!fresh) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forwarding.updatedAt = Date.now();
|
||||||
|
localStorage.setItem('oauth3.' + (forwarding.browser_state || forwarding.state), JSON.stringify(forwarding));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAction(params) {
|
||||||
|
if (params.action) {
|
||||||
|
return params.action;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.close) {
|
||||||
|
return 'close';
|
||||||
|
}
|
||||||
|
if (params.logout_callback) {
|
||||||
|
return 'logout_callback';
|
||||||
|
}
|
||||||
|
if (params.logout) {
|
||||||
|
return 'logout';
|
||||||
|
}
|
||||||
|
if (params.callback) {
|
||||||
|
return 'close';
|
||||||
|
}
|
||||||
|
if (params.directives) {
|
||||||
|
return 'directives';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'redirect';
|
||||||
|
}
|
||||||
|
|
||||||
|
incoming = parseParams();
|
||||||
|
browserState = incoming.browser_state || incoming.state;
|
||||||
|
action = parseAction(incoming);
|
||||||
|
forwarding.url = window.location.href;
|
||||||
|
forwarding.browser_state = browserState;
|
||||||
|
forwarding.state = browserState;
|
||||||
|
|
||||||
|
if (!incoming.provider_uri) {
|
||||||
|
browserCallback = incoming.callback || browserState;
|
||||||
|
} else {
|
||||||
|
// deprecated
|
||||||
|
browserCallback = 'completeLogin';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[debug]', action, incoming);
|
||||||
|
|
||||||
|
if (rpc[action]) {
|
||||||
|
rpc[action](browserState, incoming);
|
||||||
|
} else {
|
||||||
|
window.alert('unsupported action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
}());
|
|
@ -0,0 +1,9 @@
|
||||||
|
{ "authorization_dialog": { "url": "#/authorization_dialog" }
|
||||||
|
, "access_token": { "method": "POST", "url": "api/org.oauth3.provider/access_token" }
|
||||||
|
, "otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" }
|
||||||
|
, "credential_otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" }
|
||||||
|
, "credential_meta": { "url": "api/org.oauth3.provider/logins/meta/:type/:id" }
|
||||||
|
, "credential_create": { "method": "POST" , "url": "api/org.oauth3.provider/logins" }
|
||||||
|
, "grants": { "method": "GET", "url": "api/org.oauth3.provider/grants/:azp/:sub" }
|
||||||
|
, "authorization_decision": { "method": "POST", "url": "api/org.oauth3.provider/authorization_decision" }
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #ffcccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
OAuth3 RPC
|
||||||
|
|
||||||
|
<script src="/assets/com.jquery/jquery-3.1.1.js"></script>
|
||||||
|
<script src="/assets/org.oauth3/oauth3.core.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// TODO what about search within hash?
|
||||||
|
var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/]";
|
||||||
|
var params = OAUTH3_CORE.queryparse(window.location.hash || window.location.search);
|
||||||
|
if (params.debug) {
|
||||||
|
console.warn(prefix, "DEBUG MODE ENABLED. Automatic redirects disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(prefix, 'hash||search:');
|
||||||
|
console.log(window.location.hash || window.location.search);
|
||||||
|
|
||||||
|
console.log(prefix, 'params:');
|
||||||
|
console.log(params);
|
||||||
|
|
||||||
|
$.ajax({ url: 'directives.json' }).then(function (resp) {
|
||||||
|
var b64 = btoa(JSON.stringify(resp, null, 0))
|
||||||
|
var urlsafe64 = OAUTH3_CORE.utils.base64ToUrlSafeBase64(b64);
|
||||||
|
var redirect;
|
||||||
|
|
||||||
|
console.log(prefix, 'directives');
|
||||||
|
console.log(resp);
|
||||||
|
|
||||||
|
console.log(prefix, 'base64');
|
||||||
|
console.log(urlsafe64);
|
||||||
|
|
||||||
|
// TODO try postMessage back to redirect_uri domain right here
|
||||||
|
// window.postMessage();
|
||||||
|
|
||||||
|
// TODO make sure it's https NOT http
|
||||||
|
// NOTE: this can be only up to 2,083 characters
|
||||||
|
console.log(prefix, 'params.redirect_uri:', params.redirect_uri);
|
||||||
|
redirect = params.redirect_uri + '?' + OAUTH3_CORE.querystringify({
|
||||||
|
state: params.state
|
||||||
|
, directives: urlsafe64
|
||||||
|
, debug: params.debug || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(prefix, 'redirect');
|
||||||
|
console.log(redirect);
|
||||||
|
if (!params.debug) {
|
||||||
|
window.location = redirect;
|
||||||
|
} else {
|
||||||
|
// yes, we're violating the security lint with purpose
|
||||||
|
document.body.innerHTML += window.location.host + window.location.pathname
|
||||||
|
+ '<br/><br/>You\'ve passed the \'debug\' parameter so we\'re pausing'
|
||||||
|
+ ' to let you look at logs or whatever it is that you intended to do.'
|
||||||
|
+ '<br/><br/>Continue with redirect: <a href="' + redirect + '">' + redirect + '</' + 'a>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
101
README.md
101
README.md
|
@ -1,6 +1,88 @@
|
||||||
oauth3.js
|
oauth3.js
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation
|
||||||
|
(Yes! works in browsers and node.js with no extra dependencies or bloat and no hacks!)
|
||||||
|
|
||||||
|
Instead of bloating your webapp and ruining the mobile experience,
|
||||||
|
you can use a single, small javascript file for all OAuth3 providers
|
||||||
|
(and almost all OAuth2 providers) with a seemless experience.
|
||||||
|
|
||||||
|
Also, instead of complicated (or worse - insecure) CLI and Desktop login methods,
|
||||||
|
you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit)
|
||||||
|
with 0 pain.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
**Easy Install** for Web Apps (including Mobile):
|
||||||
|
|
||||||
|
1. In your web site / web app folder create a folder called `assets`
|
||||||
|
2. Inside of `assets` create another folder called `org.oauth3`
|
||||||
|
3. Download [oauth.js-v1.zip](https://git.daplie.com/Daplie/oauth3.js/repository/archive.zip?ref=v1)
|
||||||
|
4. Double-click to unzip the folder.
|
||||||
|
5. Copy `oauth3.js` and `oauth3.browser.js` to `assets/org.oauth3`
|
||||||
|
|
||||||
|
**Advanced Installation with `git`**
|
||||||
|
|
||||||
|
```
|
||||||
|
# Navigate to your web site or web app
|
||||||
|
pushd /path/to/your/web/app
|
||||||
|
|
||||||
|
|
||||||
|
# clone the project as assets/org.oauth3
|
||||||
|
mkdir -p assets
|
||||||
|
git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
|
||||||
|
pushd assests/org.oauth3
|
||||||
|
git checkout v1
|
||||||
|
popd
|
||||||
|
|
||||||
|
|
||||||
|
# symlink `.well-known/oauth3` to `assets/org.oauth3/.well-known/oauth3`
|
||||||
|
mkdir -p .well-known
|
||||||
|
ln -sf ../assets/org.oauth3/.well-known/oauth3 .well-known/oauth3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advanced Installation with `bower`**
|
||||||
|
|
||||||
|
```
|
||||||
|
# Install to bower_components
|
||||||
|
bower install oauth3
|
||||||
|
|
||||||
|
|
||||||
|
# create a `.well-known` folder and an `assets` folder
|
||||||
|
mkdir -p .well-known assets
|
||||||
|
|
||||||
|
|
||||||
|
# symlink `.well-known/oauth3` to `bower_components/oauth3/.well-known/oauth3`
|
||||||
|
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
|
||||||
|
|
||||||
|
|
||||||
|
# symlink `assets/org.oauth3` to `bower_components/oauth3`
|
||||||
|
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
|
||||||
|
ln -sf ../bower_components/oauth3 assets/org.oauth3
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Update your HTML to include the the following script tags:
|
||||||
|
|
||||||
|
```
|
||||||
|
<script src="assets/org.oauth3/oauth3.js"></script>
|
||||||
|
<script src="assets/org.oauth3/oauth3.browser.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use jQuery you should also include
|
||||||
|
|
||||||
|
```
|
||||||
|
<script src="assets/org.oauth3/oauth3.jquery.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Stable API
|
||||||
|
----------
|
||||||
|
|
||||||
Public utilities for browser and node.js:
|
Public utilities for browser and node.js:
|
||||||
|
|
||||||
* `querystringify(query)`
|
* `querystringify(query)`
|
||||||
|
@ -14,6 +96,25 @@ URL generation:
|
||||||
* `loginCode`
|
* `loginCode`
|
||||||
* `resourceOwnerPassword`
|
* `resourceOwnerPassword`
|
||||||
|
|
||||||
|
Roadmap
|
||||||
|
-------
|
||||||
|
|
||||||
|
* v1.0 - "implicit grant" authorization with examples
|
||||||
|
* popup
|
||||||
|
* iframe
|
||||||
|
* documentation
|
||||||
|
* v1.1 - cleanup
|
||||||
|
* in-flow discovery
|
||||||
|
* smallest possible size
|
||||||
|
* inline windowing (non-promisable callback)
|
||||||
|
* async set/get
|
||||||
|
* logout
|
||||||
|
* v1.2 - features
|
||||||
|
* "authorization code" flow
|
||||||
|
* "broker" flow
|
||||||
|
* v1.3 - features
|
||||||
|
* remove grants
|
||||||
|
|
||||||
URI vs URL
|
URI vs URL
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
var browser = exports.OAUTH3_BROWSER = {
|
var browser = exports.OAUTH3_BROWSER = {
|
||||||
clientUri: function (location) {
|
window: window
|
||||||
|
, clientUri: function (location) {
|
||||||
return OAUTH3_CORE.normalizeUri(location.host + location.pathname);
|
return OAUTH3_CORE.normalizeUri(location.host + location.pathname);
|
||||||
}
|
}
|
||||||
, discover: function (providerUri, opts) {
|
, discover: function (providerUri, opts) {
|
||||||
|
@ -118,18 +119,36 @@
|
||||||
resolve(tokens);
|
resolve(tokens);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}).then(function (tokens) {
|
||||||
|
return OAUTH3.hooks.refreshSession(
|
||||||
|
opts.session || {
|
||||||
|
provider_uri: providerUri
|
||||||
|
, client_id: opts.client_id
|
||||||
|
, client_uri: opts.client_uri || opts.clientUri
|
||||||
|
}
|
||||||
|
, tokens
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
, frameRequest: function (url, state, opts) {
|
, frameRequest: function (url, state, opts) {
|
||||||
var promise;
|
var promise;
|
||||||
|
|
||||||
if ('background' === opts.type) {
|
if (!opts.windowType) {
|
||||||
|
opts.windowType = 'popup';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('background' === opts.windowType) {
|
||||||
promise = browser.insertIframe(url, state, opts);
|
promise = browser.insertIframe(url, state, opts);
|
||||||
} else if ('popup' === opts.type) {
|
} else if ('popup' === opts.windowType) {
|
||||||
promise = browser.openWindow(url, state, opts);
|
promise = browser.openWindow(url, state, opts);
|
||||||
|
} else if ('inline' === opts.windowType) {
|
||||||
|
// callback function will never execute and would need to redirect back to current page
|
||||||
|
// rather than the callback.html
|
||||||
|
url += '&original_url=' + browser.window.location.href;
|
||||||
|
promise = browser.window.location = url;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("login framing method not specified or not type yet implemented");
|
throw new Error("login framing method options.windowType not specified or not type yet implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(function (params) {
|
return promise.then(function (params) {
|
||||||
|
@ -245,7 +264,7 @@
|
||||||
//
|
//
|
||||||
// Logins
|
// Logins
|
||||||
//
|
//
|
||||||
, requests: {
|
, authn: {
|
||||||
authorizationRedirect: function (providerUri, opts) {
|
authorizationRedirect: function (providerUri, opts) {
|
||||||
// TODO get own directives
|
// TODO get own directives
|
||||||
|
|
||||||
|
@ -260,9 +279,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return browser.frameRequest(prequest.url, prequest.state, opts);
|
return browser.frameRequest(prequest.url, prequest.state, opts);
|
||||||
|
}).then(function (tokens) {
|
||||||
|
return OAUTH3.hooks.refreshSession(
|
||||||
|
opts.session || {
|
||||||
|
provider_uri: providerUri
|
||||||
|
, client_id: opts.client_id
|
||||||
|
, client_uri: opts.client_uri || opts.clientUri
|
||||||
|
}
|
||||||
|
, tokens
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
, implicitGrant: function (providerUri, opts) {
|
, implicitGrant: function (providerUri, opts) {
|
||||||
|
// TODO let broker=true change behavior to open discover inline with frameRequest
|
||||||
// TODO OAuth3 provider should use the redirect URI as the appId?
|
// TODO OAuth3 provider should use the redirect URI as the appId?
|
||||||
return OAUTH3.discover(providerUri, opts).then(function (directive) {
|
return OAUTH3.discover(providerUri, opts).then(function (directive) {
|
||||||
var prequest = OAUTH3_CORE.urls.implicitGrant(
|
var prequest = OAUTH3_CORE.urls.implicitGrant(
|
||||||
|
@ -276,6 +305,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return browser.frameRequest(prequest.url, prequest.state, opts);
|
return browser.frameRequest(prequest.url, prequest.state, opts);
|
||||||
|
}).then(function (tokens) {
|
||||||
|
return OAUTH3.hooks.refreshSession(
|
||||||
|
opts.session || {
|
||||||
|
provider_uri: providerUri
|
||||||
|
, client_id: opts.client_id
|
||||||
|
, client_uri: opts.client_uri || opts.clientUri
|
||||||
|
}
|
||||||
|
, tokens
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
, logout: function (providerUri, opts) {
|
, logout: function (providerUri, opts) {
|
||||||
|
@ -455,14 +493,14 @@
|
||||||
|
|
||||||
, code: data.code
|
, code: data.code
|
||||||
|
|
||||||
, access_token: data.accessToken
|
, access_token: data.access_token
|
||||||
, expires_at: data.expiresAt
|
, expires_at: data.expires_at
|
||||||
, expires_in: data.expiresIn
|
, expires_in: data.expires_in
|
||||||
, scope: data.scope
|
, scope: data.scope
|
||||||
|
|
||||||
, refresh_token: data.refreshToken
|
, refresh_token: data.refresh_token
|
||||||
, refresh_expires_at: data.refreshExpiresAt
|
, refresh_expires_at: data.refresh_expires_at
|
||||||
, refresh_expires_in: data.refreshExpiresIn
|
, refresh_expires_in: data.refresh_expires_in
|
||||||
});
|
});
|
||||||
|
|
||||||
if ('token' === scope.appQuery.response_type) {
|
if ('token' === scope.appQuery.response_type) {
|
||||||
|
@ -507,6 +545,7 @@
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
browser.requests = browser.authn;
|
||||||
|
|
||||||
Object.keys(browser).forEach(function (key) {
|
Object.keys(browser).forEach(function (key) {
|
||||||
if ('requests' === key) {
|
if ('requests' === key) {
|
||||||
|
|
|
@ -175,10 +175,10 @@
|
||||||
, signature: parts[2] // should remain url-safe base64
|
, signature: parts[2] // should remain url-safe base64
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
, getFreshness: function (meta, staletime, now) {
|
, getFreshness: function (tokenMeta, staletime, now) {
|
||||||
staletime = staletime || (15 * 60);
|
staletime = staletime || (15 * 60);
|
||||||
now = now || Date.now();
|
now = now || Date.now();
|
||||||
var fresh = ((parseInt(meta.exp, 10) || 0) - Math.round(now / 1000));
|
var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000));
|
||||||
|
|
||||||
if (fresh >= staletime) {
|
if (fresh >= staletime) {
|
||||||
return 'fresh';
|
return 'fresh';
|
||||||
|
|
|
@ -158,7 +158,7 @@
|
||||||
|
|
||||||
var url = core.urls.resolve(directive.issuer, directive.grants.url)
|
var url = core.urls.resolve(directive.issuer, directive.grants.url)
|
||||||
.replace(/(:azp|:client_id)/g, core.normalizeUri(opts.client_id || opts.client_uri))
|
.replace(/(:azp|:client_id)/g, core.normalizeUri(opts.client_id || opts.client_uri))
|
||||||
.replace(/(:sub|:account_id)/g, opts.session.meta.sub)
|
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
|
||||||
;
|
;
|
||||||
var data = {
|
var data = {
|
||||||
client_id: opts.client_id
|
client_id: opts.client_id
|
||||||
|
@ -206,12 +206,14 @@
|
||||||
|
|
||||||
//$('.js-user-avatar').attr('src', userAvatar);
|
//$('.js-user-avatar').attr('src', userAvatar);
|
||||||
|
|
||||||
|
/*
|
||||||
console.log('grants options');
|
console.log('grants options');
|
||||||
console.log(loc.hash);
|
console.log(loc.hash);
|
||||||
console.log(loc.search);
|
console.log(loc.search);
|
||||||
console.log(clientObj);
|
console.log(clientObj);
|
||||||
console.log(session.meta);
|
console.log(session.token);
|
||||||
console.log(window.document.referrer);
|
console.log(window.document.referrer);
|
||||||
|
*/
|
||||||
|
|
||||||
return OAUTH3.requests.grants(CONFIG.host, {
|
return OAUTH3.requests.grants(CONFIG.host, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
|
@ -231,7 +233,7 @@
|
||||||
console.log(grantResults);
|
console.log(grantResults);
|
||||||
|
|
||||||
if (grantResults.data.error) {
|
if (grantResults.data.error) {
|
||||||
window.alert('grantResults: ' + grantResults.data.errorDescription || grantResults.data.error.message);
|
window.alert('grantResults: ' + grantResults.data.error_description || grantResults.data.error.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +258,7 @@
|
||||||
//return generateToken(session, clientObj);
|
//return generateToken(session, clientObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
grants = grantResults.originalData.grants.filter(function (grant) {
|
grants = (grantResults.originalData||grantResults.data).grants.filter(function (grant) {
|
||||||
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
|
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
29
oauth3.js
29
oauth3.js
|
@ -27,6 +27,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO move recase out
|
// TODO move recase out
|
||||||
|
/*
|
||||||
oauth3._recaseRequest = function (recase, req) {
|
oauth3._recaseRequest = function (recase, req) {
|
||||||
// convert JavaScript camelCase to oauth3/ruby snake_case
|
// convert JavaScript camelCase to oauth3/ruby snake_case
|
||||||
if (req.data && 'object' === typeof req.data) {
|
if (req.data && 'object' === typeof req.data) {
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
oauth3.hooks = {
|
oauth3.hooks = {
|
||||||
checkSession: function (preq, opts) {
|
checkSession: function (preq, opts) {
|
||||||
|
@ -51,7 +53,7 @@
|
||||||
console.warn('[oauth3.hooks.checkSession] no session');
|
console.warn('[oauth3.hooks.checkSession] no session');
|
||||||
return oauth3.PromiseA.resolve(null);
|
return oauth3.PromiseA.resolve(null);
|
||||||
}
|
}
|
||||||
var freshness = oauth3.core.jwt.getFreshness(preq.session.meta, opts.staletime);
|
var freshness = oauth3.core.jwt.getFreshness(preq.session.token, opts.staletime);
|
||||||
console.info('[oauth3.hooks.checkSession] freshness', freshness, preq.session);
|
console.info('[oauth3.hooks.checkSession] freshness', freshness, preq.session);
|
||||||
|
|
||||||
switch (freshness) {
|
switch (freshness) {
|
||||||
|
@ -118,11 +120,11 @@
|
||||||
oldSession.client_uri = clientUri; // azp
|
oldSession.client_uri = clientUri; // azp
|
||||||
|
|
||||||
// info about the newly-discovered token
|
// info about the newly-discovered token
|
||||||
oldSession.meta = core.jwt.decode(oldSession.access_token).payload;
|
oldSession.token = oldSession.meta = core.jwt.decode(oldSession.access_token).payload;
|
||||||
|
|
||||||
oldSession.meta.sub = oldSession.meta.sub || oldSession.meta.acx.id;
|
oldSession.token.sub = oldSession.token.sub || oldSession.token.acx.id;
|
||||||
oldSession.meta.client_uri = clientUri;
|
oldSession.token.client_uri = clientUri;
|
||||||
oldSession.meta.provider_uri = providerUri;
|
oldSession.token.provider_uri = providerUri;
|
||||||
|
|
||||||
if (oldSession.refresh_token || oldSession.refreshToken) {
|
if (oldSession.refresh_token || oldSession.refreshToken) {
|
||||||
oldSession.refresh = core.jwt.decode(oldSession.refresh_token || oldSession.refreshToken).payload;
|
oldSession.refresh = core.jwt.decode(oldSession.refresh_token || oldSession.refreshToken).payload;
|
||||||
|
@ -193,14 +195,14 @@
|
||||||
// TODO simplify (nix recase)
|
// TODO simplify (nix recase)
|
||||||
oauth3.provideRequest = function (rawRequest, opts) {
|
oauth3.provideRequest = function (rawRequest, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
var Recase = exports.Recase || require('recase');
|
//var Recase = exports.Recase || require('recase');
|
||||||
// TODO make insensitive to providing exceptions
|
// TODO make insensitive to providing exceptions
|
||||||
var recase = Recase.create({ exceptions: {} });
|
//var recase = Recase.create({ exceptions: {} });
|
||||||
|
|
||||||
function lintAndRequest(preq) {
|
function lintAndRequest(preq) {
|
||||||
function goGetHer() {
|
function goGetHer() {
|
||||||
if (preq.session) {
|
if (preq.session) {
|
||||||
// TODO check session.meta.aud against preq.url to make sure they match
|
// TODO check session.token.aud against preq.url to make sure they match
|
||||||
console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)");
|
console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)");
|
||||||
preq.headers = preq.headers || {};
|
preq.headers = preq.headers || {};
|
||||||
preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken);
|
preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken);
|
||||||
|
@ -236,9 +238,10 @@
|
||||||
return lintAndRequest(req, opts);
|
return lintAndRequest(req, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
req = oauth3._recaseRequest(recase, req);
|
//req = oauth3._recaseRequest(recase, req);
|
||||||
return lintAndRequest(req, opts).then(function (res) {
|
return lintAndRequest(req, opts).then(function (res) {
|
||||||
return oauth3._recaseResponse(recase, res);
|
//return oauth3._recaseResponse(recase, res);
|
||||||
|
return res;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -285,7 +288,7 @@
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client: oauth3.hooks.getGrants(opts.client_id + '-client')
|
client: oauth3.hooks.getGrants(opts.client_id + '-client')
|
||||||
, grants: oauth3.hooks.getGrants(opts.client_id)
|
, grants: oauth3.hooks.getGrants(opts.client_id) || []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -295,10 +298,10 @@
|
||||||
var prequest = core.urls.loginCode(directive, opts);
|
var prequest = core.urls.loginCode(directive, opts);
|
||||||
|
|
||||||
return oauth3.request(prequest).then(function (res) {
|
return oauth3.request(prequest).then(function (res) {
|
||||||
// result = { uuid, expiresAt }
|
// result = { uuid, expires_at }
|
||||||
return {
|
return {
|
||||||
otpUuid: res.data.uuid
|
otpUuid: res.data.uuid
|
||||||
, otpExpires: res.data.expiresAt
|
, otpExpires: res.data.expires_at
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.warn("What are grants? Baby don't hurt me. Don't hurt me. No more.");
|
||||||
|
console.warn(grants);
|
||||||
|
|
||||||
myGrants = grants.grants.filter(function (grant) {
|
myGrants = grants.grants.filter(function (grant) {
|
||||||
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
|
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
Loading…
Reference in New Issue