Compare commits

..

No commits in common. "master" and "pre-issuer-rewrite" have entirely different histories.

37 changed files with 2941 additions and 1598 deletions

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
prefactor
.well-known
node_modules/
DS_Store
.vscode

View File

@ -1,2 +0,0 @@
prefactor
.well-known

View File

@ -1 +1 @@
_apis
well-known

View File

@ -1,7 +0,0 @@
v1.2.2 - Works in browsers and node.js for some oauth3 exchanges
* Resource Owner Password
* Implicit Grant
* Client-side public/private keypair generation
* Server-side public key authentication
* Server-side grant storage
* BUG: Does not support app:// urls

41
LICENSE
View File

@ -1,41 +0,0 @@
Copyright 2017 Daplie, Inc
This is open source software; you can redistribute it and/or modify it under the
terms of either:
a) the "MIT License"
b) the "Apache-2.0 License"
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,12 +1,6 @@
oauth3.js
=========
| *oauth3.js*
| [issuer.html](https://git.oauth3.org/OAuth3/issuer.html)
| [issuer.rest.walnut.js](https://git.oauth3.org/OAuth3/issuer.rest.walnut.js)
| [issuer.srv](https://git.oauth3.org/OAuth3/issuer.srv)
| Sponsored by [ppl](https://ppl.family)
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!)
@ -25,12 +19,13 @@ If you have no idea what you're doing
1. Create a folder for your project named after your app, such as `example.com/`
2. Inside of the folder `example.com/` a folder called `assets/`
3. Inside of the folder `example.com/assets` a folder called `oauth3.org/`
4. Download [oauth3.js-v1.zip](https://git.oauth3.org/OAuth3/oauth3.js/repository/archive.zip?ref=v1)
3. Inside of the folder `example.com/assets` a folder called `org.oauth3/`
4. Download [oauth3.js-v1.zip](https://git.daplie.com/OAuth3/oauth3.js/repository/archive.zip?ref=v1)
5. Double-click to unzip the folder.
6. Copy the file `oauth3.core.js` into the folder `example.com/assets/oauth3.org/`
7. Copy the folder `_apis` into the folder `example.com/`
9. Add `<script src="assets/oauth3.org/oauth3.core.js"></script>` to your `index.html`
6. Copy the file `oauth3.core.js` into the folder `example.com/assets/org.oauth3/`
7. Copy the folder `well-known` into the folder `example.com/`
8. Rename the folder `well-known` to `.well-known` (when you do this, it become invisible, that's okay)
9. Add `<script src="assets/org.oauth3/oauth3.core.js"></script>` to your `index.html`
9. Add `<script src="app.js"></script>` to your `index.html`
10. Create files in `example.com` called `app.js` and `index.html` and put this in it:
@ -49,7 +44,7 @@ If you have no idea what you're doing
<script src="https://code.jquery.com/jquery-3.1.1.js"
integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
crossorigin="anonymous"></script>
<script src="assets/oauth3.org/oauth3.core.js"></script>
<script src="assets/org.oauth3/oauth3.core.js"></script>
<script src="app.js"></script>
</body>
</html>
@ -58,13 +53,13 @@ If you have no idea what you're doing
`app.js`:
```js
var OAUTH3 = window.OAUTH3;
var oauth3 = OAUTH3.create(window.location); // use window.location to set Client URI (your app's id)
var auth = OAUTH3.create(window.location); // use window.location to set Client URI (your app's id)
// this is any OAuth3-compatible provider, such as oauth3.org
// in v1.1.0 we'll add backwards compatibility for facebook.com, google.com, etc
//
function onChangeProvider(providerUri) {
function onChangeProvider(_providerUri) {
// example https://oauth3.org
return oauth3.setIdentityProvider(providerUri);
}
@ -86,13 +81,11 @@ function onClickLogin() {
console.info('Secure PPID (aka subject):', session.token.sub);
return oauth3.request({
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid'
.replace(/:sub/g, session.token.sub)
.replace(/:kid/g, session.token.kid || session.token.iss)
url: 'https://oauth3.org/api/issuer@oauth3.org/inspect_token'
, session: session
}).then(function (resp) {
console.info("Signing Public Key JWK:");
console.info("Inspect Token:");
console.log(resp.data);
});
@ -145,13 +138,13 @@ it might look like this:
example.com
├── _apis
│   └── oauth3.org
├── .well-known (hidden)
│   └── oauth3
│   ├── callback.html
│   ├── directives.json
│   └── index.html
├── assets
│   └── oauth3.org
│   └── org.oauth3
│   └── oauth3.core.js
@ -172,17 +165,17 @@ Installation (if you know what you're doing)
pushd /path/to/your/web/app
# clone the project as assets/oauth3.org
# clone the project as assets/org.oauth3
mkdir -p assets
git clone git@git.oauth3.org:OAuth3/oauth3.js.git assets/oauth3.org
pushd assets/oauth3.org
git clone git@git.daplie.com:OAuth3/oauth3.js.git assets/org.oauth3
pushd assets/org.oauth3
git checkout v1
popd
# symlink `_apis/oauth3.org` to `assets/oauth3.org/_apis/oauth3.org`
mkdir -p _apis
ln -sf ../assets/oauth3.org/_apis/oauth3 _apis/oauth3.org
# 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`**
@ -192,17 +185,17 @@ ln -sf ../assets/oauth3.org/_apis/oauth3 _apis/oauth3.org
bower install oauth3
# create a `_apis` folder and an `assets` folder
mkdir -p _apis assets
# create a `.well-known` folder and an `assets` folder
mkdir -p .well-known assets
# symlink `_apis/oauth3.org` to `bower_components/oauth3.org/_apis/oauth3.org`
ln -sf ../bower_components/oauth3.org/_apis/oauth3.org _apis/oauth3.org
# symlink `.well-known/oauth3` to `bower_components/oauth3/.well-known/oauth3`
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
# symlink `assets/oauth3.org` to `bower_components/oauth3.org`
ln -sf ../bower_components/oauth3.org/_apis/oauth3.org _apis/oauth3.org
ln -sf ../bower_components/oauth3.org assets/oauth3.org
# 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
@ -211,7 +204,7 @@ Usage
Update your HTML to include the the following script tag:
```html
<script src="assets/oauth3.org/oauth3.core.js"></script>
<script src="assets/org.oauth3/oauth3.core.js"></script>
```
You can create a very simple demo application like this:
@ -290,7 +283,7 @@ You're all set. Nothing else is needed.
We've created an `Oauth3` service just for you:
```html
<script src="assets/oauth3.org/oauth3.ng.js"></script>
<script src="assets/org.oauth3/oauth3.ng.js"></script>
```
```js
@ -323,7 +316,7 @@ promise = oauth3.init(opts); // set and fetch your own si
// promises your site's config // opts = { location, session, issuer, audience }
promise = oauth3.setIdentityProvider(url); // changes the Identity Provider URI (the site you're logging into),
// promises the provider's config // gets the config for that site (from their _apis/oauth3.org),
// promises the provider's config // gets the config for that site (from their .well-known/oauth3),
// and caches it in internal state as the default
promise = oauth3.setResourceProvider(url); // changes the Resource Provider URI (the site you're getting stuff from)
@ -340,11 +333,12 @@ promise = oauth3.request({ url, method, data }); // make an (authorized) arbi
// (contacts, photos, whatever)
promise = oauth3.api(apiname, opts); // make an (authorized) well-known api call to an audience
// Ex: oauth3.api('dns.list', { sld: 'example', tld: 'com' });
// See https://labs.daplie.com/docs/ for API schemas
// Ex: oauth3.api('dns.list', { sld: 'daplie', tld: 'com' });
// TODO
api = await oauth3.package(audience, schemaname); // make an (authorized) well-known api call to an audience
// Ex: api = await oauth3.package('domains.example.com', 'dns@oauth3.org');
// Ex: api = await oauth3.package('domains.daplie.com', 'dns@oauth3.org');
// api.list({ sld: 'mydomain', tld: 'com' });
@ -353,10 +347,6 @@ promise = oauth3.logout(); // opens logout window for t
oauth3.session(); // returns the current session, if any
```
<!-- TODO
Track down the old https://labs.daplie.com/docs/ for API schemas
--
Real API
----------
@ -498,5 +488,5 @@ can be very ugly and confusing and we definitely need to allow relative paths.
A potential work-around would be to assume all paths are relative (eliminate #4 instead)
and have the path always key off of the base URL - if oauth3 directives are to be found at
https://example.com/username/_apis/oauth3.org/index.json then /api/whatever would refer
https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer
to https://example.com/username/api/whatever.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

View File

@ -1,12 +0,0 @@
{ "terms": [ "oauth3.org/tos/draft" ]
, "api": "api.:hostname"
, "authorization_dialog": { "url": "#/authorization_dialog" }
, "access_token": { "method": "POST", "url": "api/issuer@oauth3.org/access_token" }
, "otp": { "method": "POST", "url": "api/issuer@oauth3.org/access_token/send_otp" }
, "credential_otp": { "method": "POST", "url": "api/issuer@oauth3.org/access_token/send_otp" }
, "grants": { "method": "GET", "url": "api/issuer@oauth3.org/grants/:sub/:azp" }
, "publish_jwk": { "method": "POST", "url": "api/issuer@oauth3.org/jwks/:sub" }
, "retrieve_jwk": { "method": "GET", "url": "api/issuer@oauth3.org/jwks/:sub/:kid.json" }
, "callback": { "method": "GET", "url": ".well-known/oauth3/callback.html#/" }
, "logout": { "method": "GET", "url": "#/logout/" }
}

View File

@ -1,140 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
background-color: #ffcccc;
}
</style>
</head>
<body>
OAuth3 RPC
<script src="../../assets/oauth3.org/oauth3.core.js"></script>
<script>
;(function () {
'use strict';
// Taken from oauth3.core.js
// TODO what about search within hash?
var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/]";
var params = OAUTH3.query.parse(window.location.hash || window.location.search);
var urlsafe64;
var redirect;
var err;
var oldRpc;
var sub = params.sub || params.subject;
var subData;
function doRedirect(redirect) {
if (params.debug) {
console.log(prefix, 'params.redirect_uri:', params.redirect_uri);
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>';
}
}
function onError(err) {
var redirect = params.redirect_uri + '?' + OAUTH3.query.stringify({
state: params.state
, error: err.code
, error_description: err.message
, error_uri: err.uri
, debug: params.debug || undefined
});
doRedirect(redirect);
}
function onSuccess(urlsafe64, hasSub) {
if (params.debug) {
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 SECURITY make sure it's https NOT http
// NOTE: this can be only up to 2,083 characters
redirect = params.redirect_uri + '?' + OAUTH3.query.stringify({
state: params.state
, directives: oldRpc ? urlsafe64 : undefined
, data: !oldRpc ? urlsafe64 : undefined
, sub: hasSub && sub || undefined
, debug: params.debug || undefined
});
doRedirect(redirect);
}
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);
}
if ('rpc' !== params.response_type) {
err = new Error("response_type '" + params.response_type + "' is not supported");
err.code = "E_RESPONSE_TYPE";
// TODO err.uri
onError(err);
return;
}
if (params.action) {
oldRpc = true;
}
var loco = window.location.href.replace(/\/\.well-known.*/, '');
//var loco = 'sso.hellabit.com';
var resp;
if (/localstorage/i.test(params._scheme)) {
if (sub) {
subData = localStorage.getItem(sub + '@oauth3.org:issuer');
}
resp = subData || localStorage.getItem('oauth3.org:issuer') || loco;
onSuccess(resp, subData && true);
return;
}
var fileWhiteList = [
'.well-known/oauth3/directives.json'
, '.well-known/oauth3/scopes.json'
];
if (-1 === fileWhiteList.indexOf(params._pathname)) {
err = new Error("No access to requested file: " + params._pathname);
err.code = "E_ACCESS_DENIED"
// TODO err.uri
onError(err);
}
OAUTH3.request({ url: params._pathname.replace(/^\.well-known\/oauth3\//, '') }).then(function (resp) {
urlsafe64 = OAUTH3._base64.encodeUrlSafe(JSON.stringify(resp.data, null, 0));
onSuccess(urlsafe64);
});
}());
</script>
</body>
</html>

View File

@ -1,26 +0,0 @@
{
"oauth3_authn": "Basic secure authentication"
, "auth@oauth3.org": "Basic secure authentication"
, "wallet": "Access to payments and subscriptions"
, "bucket": "Access to file storage"
, "db": "Access to app data"
, "domains": "Domain registration (and Glue and NS records)"
, "domains@oauth3.org": "Domain registration (and Glue and NS records)"
, "domains:glue": "Glue Record management (for vanity nameservers)"
, "domains:ns": "Name Server management"
, "dns": "DNS records (A/AAAA, TXT, SRV, MX, etc)"
, "hello@example.com": "Hello World Example Access"
, "authn@oauth3.org": "Basic secure authentication"
, "wallet@oauth3.org": "Access to payments and subscriptions"
, "bucket@oauth3.org": "Access to file storage"
, "db@oauth3.org": "Access to app data"
, "domains@oauth3.org": "Domain registration (and Glue and NS records)"
, "domains:glue@oauth3.org": "Glue Record management (for vanity nameservers)"
, "domains:ns@oauth3.org": "Name Server management"
, "dns@oauth3.org": "DNS records (A/AAAA, TXT, SRV, MX, etc)"
, "www@daplie.com": "Websites and webapps"
, "*": "FULL ACCOUNT ACCESS"
}

View File

@ -10,8 +10,8 @@ function parseArgs(argv, opts) {
var args = Array.prototype.slice.call(argv);
var sep = /[:\.\-]/;
args.shift(); // 'node' is the first parameter
args.shift(); // 'oauth3.js' will be the
args.shift(); // node
args.shift(); // oauth3.js
var command = args.shift() || 'help';
var cmdpair = command.split(sep);
@ -20,12 +20,12 @@ function parseArgs(argv, opts) {
var COMMAND = 'COMMAND';
var maxCmdLen = COMMAND.length;
var maxPairLen = 0;
var cmds;
var arg1 = args[0];
// build top-level commands (tlcs) list
// also count the word-width (for the space needed to print the commands)
// build commands list
var pairsMap = {};
var tlcs = opts.commands.filter(function (desc) {
cmds = opts.commands.filter(function (desc) {
var pair = desc[0].split(/\s+/)[0];
var psub = pair.split(sep)[0];
pairsMap[pair] = true;
@ -36,7 +36,13 @@ function parseArgs(argv, opts) {
}
});
// right pad (for making the printed lines longer)
if (-1 === Object.keys(pairsMap).indexOf(cmd)) {
console.log('fail', cmd);
arg1 = cmd;
cmd = 'help';
help();
}
function rpad(str, len) {
while (str.length < len) {
str += ' ';
@ -44,124 +50,45 @@ function parseArgs(argv, opts) {
return str;
}
// oauth3.js help
// oauth3.js help <command>
// oauth3.js help <command:sub> (alias of `oauth3.js <command:sub> --help')
function help() {
var status = 0;
function printCmd(desc) {
function helpMain() {
console.log('');
console.log('Here are all the top-level commands:');
console.log('');
console.log('\t' + defaults.main + ' ' + rpad(COMMAND, maxCmdLen), ' # description');
console.log('\t' + '------------------------------');
cmds.forEach(function (desc) {
var pcmd = rpad(desc[0].split(/\s+/)[0], maxCmdLen);
var pdesc = desc[1];
console.info('\t' + defaults.main + ' ' + pcmd, ' # ' + pdesc);
}
function printCmds(cmds) {
console.info('');
var title = defaults.main + ' ' + rpad(COMMAND, maxCmdLen) + ' # description';
var bars = title.replace(/./g, '-').split('');
bars[bars.length - ' # description'.length] = ' ';
bars[bars.length - (' # description'.length + 1)] = ' ';
console.info('\t' + title);
console.info('\t' + bars.join(''));
cmds.forEach(printCmd);
console.info('');
}
function helpMain() {
console.info('');
console.info('Here are all the top-level commands:');
printCmds(tlcs);
console.log('\t' + defaults.main + ' ' + pcmd, ' # ' + pdesc);
});
console.log('');
}
if (arg1 && -1 === Object.keys(pairsMap).indexOf(arg1)) {
status = 1;
console.info('');
console.info(defaults.main + ": Unknown command '" + arg1 + "'");
console.info('');
console.info("Try '" + defaults.main + " help'");
console.info('');
console.log('');
console.log(defaults.main + ": Unknown command '" + arg1 + "'");
arg1 = null;
return;
}
// the case of "oauth3 help --something"
if (!arg1 || '-' === arg1[0]) {
helpMain();
process.exit(status);
return;
}
// the case of "oauth3 help help"
if ('help' === arg1) {
helpMain();
console.info("no more help available for 'help'");
console.log("no more help available for 'help'");
process.exit(status);
return;
}
// matches the first part of the command
// and has second parts
if (arg1 === arg1.split(':')[0] && opts.commands.filter(function (desc) {
return arg1 === desc[0].split(/\s+/)[0].split(':')[0] && desc[0].split(/\s+/)[0].split(':');
}).length > 1) {
console.info('');
console.info("Here are all the '" + command + "'-related commands:");
printCmds(
opts.commands.filter(function (desc) {
var pair = desc[0].split(/\s+/)[0];
var psub = pair.split(sep)[0];
maxPairLen = Math.max(maxPairLen, pair.length);
if (arg1 === psub || arg1 === pair) {
maxCmdLen = Math.max(maxCmdLen, pair.length);
return true;
}
})
);
console.info('');
} else {
console.info('');
console.info("Here are all the options and flags for '" + arg1 + "':");
console.info('');
opts.commands.some(function (desc) {
var pair = desc[0].split(/\s+/)[0];
var psub = pair.split(sep)[0];
maxPairLen = Math.max(maxPairLen, pair.length);
if (arg1 !== psub && arg1 !== pair) {
return false;
}
maxCmdLen = Math.max(maxCmdLen, pair.length);
console.log('\t' + desc[0] + '\t# ' + desc[1]);
(desc[2]||[]).forEach(function (flag) {
var pair = flag.split(', ');
var f = pair.shift();
var d = pair.join(', ');
console.log('\t\t' + f + ' # ' + d);
});
return true;
});
console.info('');
}
}
// If the command is not in the list of commands
if (-1 === Object.keys(pairsMap).indexOf(cmd)) {
arg1 = cmd;
cmd = 'help';
help();
return;
}
// If help is explictly requested
if (-1 !== [ 'help', '-h', '--help' ].indexOf(command) || -1 !== args.indexOf('-h') || -1 !== args.indexOf('--help')) {
help();
return;
}
// If we're ready to rock and roll!
console.log('RUN', cmd, sub || '(n/a)', arg1 || '(n/a)', '... not yet implemented');
}
parseArgs(process.argv, {
@ -204,8 +131,8 @@ parseArgs(process.argv, {
// authn / authz
, [ 'devices', 'manages devices for your account(s)' ]
, [ 'devices:new', 'create a new device (default name is hostname, default ip is the result of :provider/api/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:set', 'set the ip address of the device (defaults ip is the result of :provider/api/tunnel@oauth3.org/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:new', 'create a new device (default name is hostname, default ip is the result of :provider/api/org.oauth3.tunnel/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:set', 'set the ip address of the device (defaults ip is the result of :provider/api/org.oauth3.tunnel/checkip)'.replace(/\b:provider\b/, defaults.provider) ]
, [ 'devices:attach', "attach a device to a domain's DNS record" ]
, [ 'devices:detach', "detach an account from a domain's DNS record" ]
, [ 'devices:select', '(re)claim the specified device as this device (i.e. you re-installed your OS or deleted your ~/.oauth3)' ]

View File

@ -26,7 +26,7 @@
"sign"
],
"license": "MIT",
"homepage": "https://git.oauth3.org/OAuth3/oauth3.js",
"homepage": "https://git.daplie.com/OAuth3/oauth3.js",
"ignore": [
"**/.*",
"browserify",

14
bump-versions.sh Normal file
View File

@ -0,0 +1,14 @@
git push --tags
git checkout v1.0
git push
git checkout v1
git merge v1.0
git push
git checkout master
git merge v1
git push
git checkout v1.0

View File

@ -1,96 +0,0 @@
(function () {
'use strict';
function create(myOpts) {
return {
requestScope: function (opts) {
// TODO pre-generate URL
// deliver existing session if it exists
var scope = opts && opts.scope || [];
if (myOpts.session) {
if (!scope.length || scope.every(function (scp) {
return -1 !== opts.myOpts.session.scope.indexOf(scp);
})) {
return OAUTH3.PromiseA.resolve(myOpts.session);
}
}
// request a new session otherwise
return OAUTH3.implicitGrant(myOpts.directives, {
client_id: myOpts.conf.client_uri
, client_uri: myOpts.conf.client_uri
// maybe use inline instead?
, windowType: 'popup'
, scope: scope
}).then(function (session) {
return session;
});
}
, session: function () {
return myOpts.session;
}
, refresh: function (session) {
return OAUTH3.implicitGrant(myOpts.directives, {
client_id: myOpts.conf.client_uri
, client_uri: myOpts.conf.client_uri
, windowType: 'background'
}).then(function (_session) {
session = _session;
return session;
});
}
, logout: function () {
return OAUTH3.logout(myOpts.directives, {
client_id: myOpts.conf.client_uri
, client_uri: myOpts.conf.client_uri
});
}
, switchUser: function () {
// should open dialog with user selection dialog
}
}
}
window.navigator.auth = {
getUserAuthenticator: function (opts) {
var conf = {};
var directives;
var session;
opts = opts || {};
conf.client_uri = opts.client_uri || OAUTH3.clientUri(opts.location || window.location);
return OAUTH3.issuer({ broker: opts.issuer_uri || 'https://new.oauth3.org' }).then(function (issuer) {
conf.issuer_uri = issuer;
conf.provider_uri = issuer;
return OAUTH3.directives(conf.provider_uri, {
client_id: conf.client_uri
, client_uri: conf.client_uri
}).then(function (_directives) {
directives = _directives;
var myOpts = {
directives: directives
, conf: conf
};
return OAUTH3.implicitGrant(directives, {
client_id: conf.client_uri
, client_uri: conf.client_uri
, windowType: 'background'
}).then(function (_session) {
session = _session;
myOpts.session = session;
return create(myOpts);
}, function (err) {
console.error('[DEBUG] implicitGrant err:');
console.error(err);
return create(myOpts);
});
});
});
}
};
}());

View File

@ -1,4 +1,4 @@
/ * global Promise */
/* global Promise */
;(function (exports) {
'use strict';
@ -78,7 +78,7 @@
, uri: {
normalize: function (uri) {
if ('string' !== typeof uri) {
throw new Error("attempted to normalize non-string URI: "+JSON.stringify(uri));
throw new Error('must provide a string to OAUTH3.uri.normalize');
}
// tested with
// example.com
@ -94,7 +94,7 @@
, url: {
normalize: function (url) {
if ('string' !== typeof url) {
throw new Error("attempted to normalize non-string URL: "+JSON.stringify(url));
throw new Error('must provide a string to OAUTH3.url.normalize');
}
// tested with
// example.com
@ -168,12 +168,9 @@
}
}
, scope: {
parse: function (scope) {
return (scope||'').toString().split(/[+, ]+/g);
}
, stringify: function (scope) {
stringify: function (scope) {
if (Array.isArray(scope)) {
scope = scope.join(',');
scope = scope.join(' ');
}
return scope;
}
@ -207,68 +204,25 @@
}
, jwt: {
// decode only (no verification)
decode: function (token, opts) {
decode: function (str) {
// 'abc.qrs.xyz'
// [ 'abc', 'qrs', 'xyz' ]
// {}
var parts = token.split(/\./g);
var err;
if (parts.length !== 3) {
err = new Error("Invalid JWT: required 3 '.' separated components not "+parts.length);
err.code = 'E_INVALID_JWT';
throw err;
}
// [ {}, {}, 'foo' ]
// { header: {}, payload: {}, signature: '' }
var parts = str.split(/\./g);
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64));
});
if (!opts || !opts.complete) {
return JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1]));
return { header: jsons[0], payload: jsons[1] };
}
return {
header: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[0]))
, payload: JSON.parse(OAUTH3._base64.decodeUrlSafe(parts[1]))
};
}
, verify: function (token, jwk) {
if (!OAUTH3.crypto) {
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
}
jwk = jwk.publicKey || jwk;
, verify: function (jwk, token) {
var parts = token.split(/\./g);
var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]);
return OAUTH3.crypto.core.verify(jwk, data, signature).then(function () {
return OAUTH3.jwt.decode(token);
});
}
, sign: function (payload, jwk) {
if (!OAUTH3.crypto) {
return OAUTH3.PromiseA.reject(new Error("OAuth3 crypto library unavailable"));
}
jwk = jwk.private_key || jwk.privateKey || jwk;
var prom;
if (jwk.kid) {
prom = OAUTH3.PromiseA.resolve(jwk.kid);
} else {
prom = OAUTH3.crypto.thumbprintJwk(jwk);
}
return prom.then(function (kid) {
// Currently the crypto part of the OAuth3 library only supports ES256
var header = {type: 'JWT', alg: 'ES256', kid: kid};
var input = [
OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null))
, OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null))
].join('.');
return OAUTH3.crypto.core.sign(jwk, OAUTH3._binStr.binStrToBuffer(input))
.then(OAUTH3._base64.bufferToUrlSafe)
.then(function (signature) {
return input + '.' + signature;
});
});
return OAUTH3.crypto.core.verify(jwk, data, signature);
}
, freshness: function (tokenMeta, staletime, now) {
// If the token doesn't expire then it's always fresh.
@ -294,53 +248,41 @@
}
}
, urls: {
rpc: function (providerUri, opts) {
discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error("cannot run rpc without providerUri");
throw new Error("cannot discover without providerUri");
}
if (!opts.client_id) {
throw new Error("cannot run rpc without options.client_id");
throw new Error("cannot discover without options.client_id");
}
var clientId = OAUTH3.url.normalize(opts.client_id || opts.client_uri);
providerUri = OAUTH3.url.normalize(providerUri);
var params = {
state: opts.state || OAUTH3.utils.randomState()
action: 'directives'
, state: opts.state || OAUTH3.utils.randomState()
, redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/')
, response_type: 'rpc'
, _method: 'GET'
, _scheme: opts._scheme
, _pathname: opts._pathname
, _pathname: '.well-known/oauth3/directives.json'
, debug: opts.debug || undefined
};
var toRequest = {
var result = {
url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.query.stringify(params)
, state: params.state
, method: 'GET'
, query: params
};
return toRequest;
}
, broker: function (providerUri, opts) {
opts._scheme = "localstorage:";
opts._pathname = "issuer";
return OAUTH3.urls.rpc(providerUri, opts);
}
, discover: function (providerUri, opts) {
return OAUTH3.urls.directives(providerUri, opts);
}
, directives: function (providerUri, opts) {
opts._pathname = ".well-known/oauth3/scopes.json";
return OAUTH3.urls.rpc(providerUri, opts);
return result;
}
, implicitGrant: function (directive, opts) {
//
// Example Implicit Grant Request
// (for generating a browser-only session, not a session on your server)
//
// GET https://example.com/api/issuer@oauth3.org/authorization_dialog
// GET https://example.com/api/org.oauth3.provider/authorization_dialog
// ?response_type=token
// &scope=`encodeURIComponent('profile.login profile.email')`
// &state=`cryptoutil.random().toString('hex')`
@ -406,36 +348,29 @@
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var refresh_token = opts.refresh_token || (opts.session && opts.session.refresh_token);
var err;
if (!refresh_token) {
err = new Error('refreshing a token requires a refresh token');
err.code = 'E_NO_TOKEN';
throw err;
}
if (OAUTH3.jwt.freshness(OAUTH3.jwt.decode(refresh_token)) === 'expired') {
err = new Error('refresh token has also expired, login required again');
err.code = 'E_EXPIRED_TOKEN';
throw err;
}
var type = 'access_token';
var grantType = 'refresh_token';
var scope = opts.scope || directive.authn_scope;
var args = directive.access_token;
var clientSecret = opts.client_secret;
var args = directive[type];
var params = {
"grant_type": 'refresh_token'
, "refresh_token": refresh_token
"grant_type": grantType
, "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token)
, "response_type": 'token'
, "client_id": opts.client_id || opts.client_uri
, "client_uri": opts.client_uri
//, "scope": undefined
//, "client_secret": undefined
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (opts.client_secret) {
if (clientSecret) {
// TODO not allowed in the browser
console.warn("if this is a browser, you must not use client_secret");
params.client_secret = opts.client_secret;
params.client_secret = clientSecret;
}
if (scope) {
@ -502,52 +437,42 @@
}
}
, hooks: {
_checkStorage: function (grpName, funcName) {
if (!OAUTH3._hooks) {
OAUTH3._hooks = {};
}
if (!OAUTH3._hooks[grpName]) {
console.log('using default storage for '+grpName+', set OAUTH3._hooks.'+grpName+' for custom storage');
OAUTH3._hooks[grpName] = OAUTH3._defaultStorage[grpName];
}
if (!OAUTH3._hooks[grpName][funcName]) {
throw new Error("'"+funcName+"' is not defined for custom "+grpName+" storage");
}
}
, directives: {
directives: {
get: function (providerUri) {
OAUTH3.hooks._checkStorage('directives', 'get');
if (!providerUri) {
throw new Error("providerUri is not set");
providerUri = OAUTH3.uri.normalize(providerUri);
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._getCached(providerUri)
|| OAUTH3.hooks.directives._get(providerUri))
.then(function (directives) {
// or do .then(this._set) to keep DRY?
OAUTH3.hooks.directives._cache[providerUri] = directives;
return directives;
});
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.get(OAUTH3.uri.normalize(providerUri)));
, _getCached: function (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
return OAUTH3.hooks.directives._cache[providerUri];
}
, set: function (providerUri, directives) {
OAUTH3.hooks._checkStorage('directives', 'set');
if (!providerUri) {
throw new Error("providerUri is not set");
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; }
OAUTH3.hooks.directives._cache[providerUri] = directives;
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._set(providerUri, directives));
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.set(OAUTH3.uri.normalize(providerUri), directives));
, _get: function (providerUri) {
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.get) {
console.warn('[Warn] Please implement OAUTH3._hooks.directives.get = function (providerUri) { return PromiseA<directives>; }');
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
}
, all: function () {
OAUTH3.hooks._checkStorage('directives', 'all');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.all());
return OAUTH3._hooks.directives.get(providerUri);
}
, clear: function () {
OAUTH3.hooks._checkStorage('directives', 'clear');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.directives.clear());
, _set: function (providerUri, directives) {
if (!OAUTH3._hooks || !OAUTH3._hooks.directives || !OAUTH3._hooks.directives.set) {
console.warn('[Warn] Please implement OAUTH3._hooks.directives.set = function (providerUri, directives) { return PromiseA<directives>; }');
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
return directives;
}
}
, scopes: {
get: function(providerUri) {
//TODO: retrieve cached scopes
}
, set: function(providerUri, scopes) {
//TODO: cache scopes
return OAUTH3._hooks.directives.set(providerUri, directives);
}
}
, session: {
@ -567,7 +492,7 @@
oldSession.client_uri = clientUri; // azp
// info about the newly-discovered token
oldSession.token = OAUTH3.jwt.decode(oldSession.access_token);
oldSession.token = OAUTH3.jwt.decode(oldSession.access_token).payload;
oldSession.token.sub = oldSession.token.sub
|| (oldSession.token.acx||{}).id
@ -578,7 +503,7 @@
oldSession.token.provider_uri = providerUri;
if (oldSession.refresh_token) {
oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token);
oldSession.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload;
oldSession.refresh.sub = oldSession.refresh.sub
|| (oldSession.refresh.acx||{}).id
|| ((oldSession.refresh.axs||[])[0]||{}).appScopedId
@ -588,7 +513,7 @@
}
// set for a set of audiences
return OAUTH3.hooks.session.set(providerUri, oldSession);
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession));
}
, check: function (preq, opts) {
opts = opts || {};
@ -641,66 +566,68 @@
return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession);
});
}
, _getCached: function (providerUri, id) {
providerUri = OAUTH3.uri.normalize(providerUri);
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; }
if (id) {
return OAUTH3.hooks.session._cache[providerUri + id];
}
return OAUTH3.hooks.session._cache[providerUri];
}
, set: function (providerUri, newSession, id) {
OAUTH3.hooks._checkStorage('sessions', 'set');
if (!providerUri) {
console.error(new Error('no providerUri').stack);
throw new Error("providerUri is not set");
}
providerUri = OAUTH3.uri.normalize(providerUri);
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.set(providerUri, newSession, id));
if (!OAUTH3.hooks.session._cache) { OAUTH3.hooks.session._cache = {}; }
OAUTH3.hooks.session._cache[providerUri + (id || newSession.id || newSession.token.id || '')] = newSession;
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = newSession;
}
return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._set(providerUri, newSession));
}
, get: function (providerUri, id) {
OAUTH3.hooks._checkStorage('sessions', 'get');
providerUri = OAUTH3.uri.normalize(providerUri);
if (!providerUri) {
throw new Error("providerUri is not set");
}
providerUri = OAUTH3.uri.normalize(providerUri);
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id));
}
, all: function (providerUri) {
OAUTH3.hooks._checkStorage('sessions', 'all');
if (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
return OAUTH3.PromiseA.resolve(
OAUTH3.hooks.session._getCached(providerUri, id) || OAUTH3.hooks.session._get(providerUri, id)
).then(function (session) {
var s = session || { token: {} };
OAUTH3.hooks.session._cache[providerUri + (id || s.id || s.token.id || '')] = session;
if (!id) {
OAUTH3.hooks.session._cache[providerUri] = session;
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.all(providerUri));
}
, clear: function (providerUri) {
OAUTH3.hooks._checkStorage('sessions', 'clear');
if (providerUri) {
providerUri = OAUTH3.uri.normalize(providerUri);
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.clear(providerUri));
}
}
}
, discoverScopes: function (providerUri, opts) {
return OAUTH.scopes(providerUri, opts);
}
, scopes: function (providerUri, opts) {
if (!providerUri) {
throw new Error('oauth3.discoverScopes(providerUri, opts) received providerUri as :', providerUri);
}
opts = opts || {};
opts._pathname = ".well-known/oauth3/scopes.json";
//TODO: add caching
return OAUTH3._rpcHelper(providerUri, opts).then(function(scopes) {
return scopes;
return session;
});
}
, discover: function (providerUri, opts) {
return OAUTH3.directives(providerUri, opts);
, _get: function (providerUri, id) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.all) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.all = function ([providerUri]) { return PromiseA<sessions>; }');
}
, directives: function (providerUri, opts) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.get) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.get = function (providerUri[, id]) { return PromiseA<session>; }');
return JSON.parse(window.sessionStorage.getItem('session-' + providerUri + (id || '')) || 'null');
}
return OAUTH3._hooks.sessions.get(providerUri, id);
}
, _set: function (providerUri, newSession, id) {
if (!OAUTH3._hooks || !OAUTH3._hooks.sessions || !OAUTH3._hooks.sessions.set) {
console.warn('[Warn] Please implement OAUTH3._hooks.sessions.set = function (providerUri, newSession[, id]) { return PromiseA<newSession>; }');
window.sessionStorage.setItem('session-' + providerUri, JSON.stringify(newSession));
window.sessionStorage.setItem('session-' + providerUri + (id || newSession.id || newSession.token.id || ''), JSON.stringify(newSession));
return newSession;
}
return OAUTH3._hooks.sessions.set(providerUri, newSession, id);
}
}
}
, discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error('oauth3.discover(providerUri, opts) received providerUri as :', providerUri);
throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
}
return OAUTH3.hooks.directives.get(providerUri).then(function (directives) {
@ -708,8 +635,7 @@
return directives;
}
opts._pathname = ".well-known/oauth3/directives.json";
return OAUTH3._rpcHelper(providerUri, opts).then(function (directives) {
return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) {
directives.azp = directives.azp || OAUTH3.url.normalize(providerUri);
directives.issuer = directives.issuer || OAUTH3.url.normalize(providerUri);
directives.api = OAUTH3.url.normalize((directives.api||':hostname').replace(/:hostname/, OAUTH3.uri.normalize(directives.issuer) || OAUTH3.uri.normalize(providerUri)));
@ -718,8 +644,8 @@
});
});
}
, _rpcHelper: function(providerUri, opts) {
return OAUTH3._browser.rpc(providerUri, opts);
, _discoverHelper: function(providerUri, opts) {
return OAUTH3._browser.discover(providerUri, opts);
}
, request: function (preq, opts) {
function fetch() {
@ -747,22 +673,7 @@
*/
return OAUTH3._browser.request(preq, opts);
}
, issuer: function (opts) {
if (!opts) { opts = {}; }
// TODO this will default to browserlogin.org
var broker = opts.broker || 'https://new.oauth3.org';
//var broker = opts.broker || 'https://broker.oauth3.org';
opts._rpc = "broker";
opts._scheme = "localstorage:";
opts._pathname = "issuer";
return OAUTH3._rpcHelper(broker, opts).then(function(issuer) {
return issuer;
});
}
, implicitGrant: function (directives, opts) {
, implicitGrant: function(directives, opts) {
var promise;
var providerUri = directives.azp || directives.issuer || directives;
@ -773,9 +684,7 @@
}
else {
// Discovery must take place before calling implicitGrant
promise = OAUTH3.hooks.directives.get(providerUri).then(function (directives) {
return OAUTH3._implicitGrant(directives, opts);
});
promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts);
}
return promise.then(function (tokens) {
@ -872,23 +781,14 @@
});
});
}
, logout: function(issuerUri, opts) {
var directives;
if ('string' !== typeof issuerUri) {
directives = issuerUri;
return OAUTH3._logoutHelper(directives, opts);
}
return OAUTH3.hooks.directives.get(issuerUri).then(function (directives) {
return OAUTH3._logoutHelper(directives, opts);
});
, logout: function(providerUri, opts) {
return OAUTH3._logoutHelper(OAUTH3.hooks.directives._getCached(providerUri), opts);
}
, _logoutHelper: function(directives, opts) {
var issuerUri = directives.issuer_uri || directives.provider_uri;
var logoutReq = OAUTH3.urls.logout(
directives
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
, windowType: 'popup' // TODO: figure out background later
, windowType: 'popup' // we'll figure out background later
, broker: opts.broker
//, state: opts._state
, debug: opts.debug
@ -907,10 +807,10 @@
if (params.error) {
// TODO directives.audience
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*issuerUri*/, params));
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
}
OAUTH3.hooks.session.clear(issuerUri);
OAUTH3.hooks.session._cache = {};
return params;
});
}
@ -921,50 +821,40 @@
//
, _browser: {
window: 'undefined' !== typeof window ? window : null
, rpc: function(providerUri, opts) {
// TODO we don't need to include this if we're using jQuery or angular
, discover: function(providerUri, opts) {
opts = opts || {};
providerUri = OAUTH3.url.normalize(providerUri);
// TODO SECURITY should we whitelist our own self?
if (OAUTH3.uri.normalize(providerUri).replace(/\/.*/, '') === OAUTH3.uri.normalize(OAUTH3._browser.window.location.hostname)) {
console.warn("It looks like you're a provider trying to run rpc on yourself,"
console.warn("It looks like you're a provider checking for your own directive,"
+ " so we we're just gonna use"
+ " OAUTH3.request({ method: 'GET', url: "
+ "'" + opts._pathname + "' })");
if (/localstorage/i.test(opts._scheme)) {
return OAUTH3.PromiseA.resolve(localStorage.getItem(opts._pathname));
}
else {
+ " OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })");
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.url.normalize(providerUri) + '/' + opts._pathname // '/.well-known/oauth3/' + discoverFile
, url: OAUTH3.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
}).then(function (resp) {
return resp.data;
});
}
}
if (!(opts.client_id || opts.client_uri || '').match(OAUTH3._browser.window.location.hostname)) {
if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) {
console.warn("It looks like your client_id doesn't match your current window..."
+ " this probably won't end well");
console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname);
}
var discReq = OAUTH3.urls[opts._rpc || 'rpc'](
var discReq = OAUTH3.urls.discover(
providerUri
, { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location))
, windowType: opts.broker && opts.windowType || 'background'
, broker: opts.broker
, state: opts._state || undefined
, debug: opts.debug
, _scheme: opts._scheme
, _pathname: opts._pathname
, _method: opts._method
}
);
opts._state = discReq.state;
//var discReq = OAUTH3.urls.rpc(providerUri, opts);
//var discReq = OAUTH3.urls.discover(providerUri, opts);
// hmm... we're gonna need a broker for this since switching windows is distracting,
// popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
@ -974,7 +864,6 @@
// TODO allow node to open a desktop browser window
opts._windowType = opts.windowType;
opts.windowType = opts.windowType || 'background';
return OAUTH3._browser.testPixel(providerUri).then(function () {
return OAUTH3._browser.frameRequest(
OAUTH3.url.resolve(providerUri, discReq.url)
, discReq.state
@ -993,17 +882,9 @@
}
// TODO params should have response_type indicating json, binary, etc
var result;
try {
result = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.data || params.result || params.directives));
} catch(e) {
result = params.data || params.result;
}
console.log('result:', result);
var directives = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.result || params.directives));
// caller will call OAUTH3.hooks.directives.set(providerUri, directives);
return result;
});
return directives;
});
}
, request: function (preq, _sys) {
@ -1036,12 +917,9 @@
if (data.error) {
err = new Error(data.error.message || data.error_description || JSON.stringify(data.error));
Object.assign(err, data.error);
}
if (err) {
err._request = xhr;
err.status = xhr.status;
err.data = data;
err.result = data;
reject(err);
return;
}
@ -1053,11 +931,6 @@
, status: xhr.status
});
};
xhr.ontimeout = function () {
var err = new Error('ETIMEDOUT');
err.code = 'ETIMEDOUT';
reject(err);
};
if (preq.progress) {
xhr.upload.onprogress = function (ev) {
@ -1075,9 +948,6 @@
// For assets.example.com/assets
xhr.withCredentials = true;
if (preq.timeout) {
xhr.timeout = preq.timeout;
}
if (preq.data) {
headers['Content-Type'] = 'application/json'; // TODO XXX TODO utf8
}
@ -1102,28 +972,6 @@
}
});
}
, testPixel: function (targetUri) {
var url = OAUTH3.url.resolve(OAUTH3.url.normalize(targetUri), '.well-known/oauth3/clear.gif');
return new OAUTH3.PromiseA(function (resolve, reject) {
var img = document.createElement('img');
img.addEventListener('load', function () {
resolve();
});
img.addEventListener('error', function () {
var err = new Error("OAuth3 support not detected: '" + url + "' not found");
err.code = 'E_NOT_SUPPORTED';
reject(err);
});
// works with CSP
img.style.position = 'absolute';
img.style.left = '-2px';
img.style.bottom = '-2px';
img.className = 'js-oauth3-discover';
img.src = url;
document.body.appendChild(img);
console.log('img', img);
});
}
, frameRequest: function (url, state, opts) {
opts = opts || {};
var previousFrame = OAUTH3._browser._frames[state];
@ -1134,10 +982,11 @@
}
var timeout = opts.timeout;
if ('background' === windowType) {
if (!timeout) {
timeout = 7 * 1000;
if (opts.debug) {
timeout = timeout || 3 * 60 * 1000;
}
else {
timeout = timeout || ('background' === windowType ? 15 * 1000 : 3 * 60 * 1000);
}
return new OAUTH3.PromiseA(function (resolve, reject) {
@ -1159,7 +1008,6 @@
cleanup();
};
if (timeout) {
tok = setTimeout(function () {
var err = new Error(
"the '" + windowType + "' request did not complete within " + Math.round(timeout / 1000) + "s"
@ -1168,7 +1016,6 @@
reject(err);
cleanup();
}, timeout);
}
setTimeout(function () {
if (!OAUTH3._browser._frames[state]) {
@ -1291,103 +1138,10 @@
};
OAUTH3.login = OAUTH3.implicitGrant;
OAUTH3._defaultStorage = {
_getStorageKeys: function (prefix, storage) {
storage = storage || window.localStorage;
var matching = [];
var ind, key;
for (ind = 0; ind < storage.length; ind++) {
key = storage.key(ind);
if (key.indexOf(prefix || '') === 0) {
matching.push(key);
}
}
return matching;
}
, directives: {
prefix: 'directives-'
, get: function (providerUri) {
var result = JSON.parse(window.localStorage.getItem(this.prefix + providerUri) || '{}');
return OAUTH3.PromiseA.resolve(result);
}
, set: function (providerUri, directives) {
window.localStorage.setItem(this.prefix + providerUri, JSON.stringify(directives));
return this.get(providerUri);
}
, all: function () {
var prefix = this.prefix;
var result = {};
OAUTH3._defaultStorage._getStorageKeys(prefix).forEach(function (key) {
result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || '{}');
});
return OAUTH3.PromiseA.resolve(result);
}
, clear: function () {
OAUTH3._defaultStorage._getStorageKeys(this.prefix).forEach(function (key) {
window.localStorage.removeItem(key);
});
return OAUTH3.PromiseA.resolve();
}
}
, sessions: {
prefix: 'session-'
, get: function (providerUri, id) {
var result;
if (id) {
result = JSON.parse(window.sessionStorage.getItem(this.prefix + providerUri+id) || 'null');
} else {
result = JSON.parse(window.sessionStorage.getItem(this.prefix + providerUri) || 'null');
}
return OAUTH3.PromiseA.resolve(result);
}
, set: function (providerUri, newSession, id) {
var str = JSON.stringify(newSession);
window.sessionStorage.setItem(this.prefix + providerUri, str);
id = id || newSession.id || newSession.token.sub || newSession.token.id;
if (id) {
window.sessionStorage.setItem(this.prefix + providerUri + id, str);
}
return this.get(providerUri, id);
}
, all: function (providerUri) {
var prefix = this.prefix + (providerUri || '');
var result = {};
OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) {
result[key.replace(prefix, '')] = JSON.parse(window.sessionStorage.getItem(key) || 'null');
});
return OAUTH3.PromiseA.resolve(result);
}
, clear: function (providerUri) {
var prefix = this.prefix + (providerUri || '');
OAUTH3._defaultStorage._getStorageKeys(prefix, window.sessionStorage).forEach(function (key) {
window.sessionStorage.removeItem(key);
});
return OAUTH3.PromiseA.resolve();
}
}
};
// TODO get rid of these
OAUTH3.utils = {
clientUri: OAUTH3.clientUri
, query: OAUTH3.query
, parseSubject: function (sub) {
var parts = sub.split('@');
var issuer;
var subject;
if (/@/.test(sub)) {
// The username may have a single @, the provider may not
// user@thing.com@whatever.com -> user@thing.com, whatever.com
issuer = parts.pop();
subject = parts.join('@');
} else {
//subject = '';
issuer = parts.join('@');
}
return { subject: subject, issuer: issuer };
}
, scope: OAUTH3.scope
, uri: OAUTH3.uri
, url: OAUTH3.url
@ -1415,7 +1169,7 @@
, _resourceProviderUri: null
, _identityProviderDirectives: null
, _resourceProviderDirectives: null
//, _resourceProviderMap: null // map between xyz.com and domains@oauth3.org
//, _resourceProviderMap: null // map between xyz.com and org.oauth3.domains
, _init: function (location, opts) {
var me = this;
if (!opts) {
@ -1484,7 +1238,7 @@
var me = this;
return me._initClient().then(function () {
return me.setIdentityProvider(providerUri).then(function () {
// TODO how to say "Use xyz.com for domains@oauth3.org, but abc.com for dns@oauth3.org"?
// TODO how to say "Use xyz.com for org.oauth3.domains, but abc.com for org.oauth3.dns"?
return me.setResourceProvider(providerUri);
});
});
@ -1516,10 +1270,9 @@
}
, login: function (opts) {
var me = this;
return OAUTH3.hooks.session.get(me._identityProviderUri).then(function (session) {
if (session) {
if (me.session()) {
me._session = true;
return session;
return OAUTH3.PromiseA.resolve(me.session());
}
opts = opts || {};
@ -1529,11 +1282,10 @@
me._session = true;
return session;
});
});
}
, session: function (session, id) {
if (!session) {
return OAUTH3.hooks.session.get(this._identityProviderUri);
return JSON.parse(JSON.stringify(OAUTH3.hooks.session._getCached(this._identityProviderUri) || null));
}
return OAUTH3.hooks.session.set(this._identityProviderUri, session, id);
}
@ -1542,39 +1294,29 @@
preq.client_uri = this._clientUri;
preq.client_id = this._clientUri;
preq.method = preq.method || 'GET';
if (this._session) {
preq.session = preq.session || this.session(); // OAUTH3.hooks.session._getCached(this._identityProviderUri);
}
// TODO maybe use a baseUrl from the directives file?
preq.url = OAUTH3.url.resolve(this._resourceProviderUri, preq.url);
if (preq.session || !this._session) {
return OAUTH3.request(preq, opts);
}
return this.session().then(function (session) {
preq.session = session;
return OAUTH3.request(preq, opts);
});
}
, logout: function (opts) {
var me = this;
me._session = false;
this._session = false;
opts = opts || {};
return OAUTH3.hooks.session.get(me._identityProviderUri).then(function (session) {
opts.client_uri = me._clientUri;
opts.client_id = me._clientUri;
opts.session = session;
opts.client_uri = this._clientUri;
opts.client_id = this._clientUri;
opts.session = OAUTH3.hooks.session._getCached(this._identityProviderUri);
return OAUTH3.logout(me._identityProviderUri, opts);
});
return OAUTH3.logout(this._identityProviderUri, opts);
}
, api: function (api, opts) {
var me = this;
opts = opts || {};
return OAUTH3.hooks.session.get(me._identityProviderUri).then(function (session) {
opts.api = api;
opts.session = session;
opts.session = OAUTH3.hooks.session._getCached(this._identityProviderUri);
return OAUTH3.api(me._resourceProviderDirectives.api, opts);
});
return OAUTH3.api(this._resourceProviderDirectives.api, opts);
}
, pkg: function (pkgname) {
var me = this;
@ -1588,19 +1330,17 @@
return OAUTH3.PromiseA.reject(new Error("No Package for '" + pkgname + "'"));
}
return OAUTH3.hooks.session.get(issuer).then(function (session) {
pkg = OAUTH3._pkgs[pkgname];
Object.keys(pkg).forEach(function (key) {
result[key] = function (opts) {
opts = opts || {};
opts.session = session;
opts.session = OAUTH3.hooks.session._getCached(issuer);
opts.audience = audience;
return pkg[key](opts);
};
});
return result;
});
}
};
result.setIssuer = result.setIdentityProvider;

View File

@ -1,5 +1,5 @@
;(function (exports) {
'use strict';
'use strict';
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
@ -8,9 +8,6 @@
OAUTH3.crypto.core = require('./oauth3.node.crypto');
} catch (error) {
OAUTH3.crypto.core = {};
OAUTH3.crypto.core.ready = false;
var finishBeforeReady = [];
var deferedCalls = [];
// We don't currently have a fallback method for this function, so we assign
// it directly to the core object instead of the webCrypto object.
@ -20,31 +17,10 @@
};
var webCrypto = {};
var deferCryptoCall = function(name) {
return function() {
var args = arguments;
return new OAUTH3.PromiseA(function(resolve, reject) {
deferedCalls.push(function(){
try {
webCrypto[name].apply(webCrypto, args)
.then(function(result){
resolve(result);
});
} catch(e) {
reject(e);
}
});
});
};
};
OAUTH3.crypto.core.sha256 = deferCryptoCall("sha256");
webCrypto.sha256 = function (buf) {
return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
};
OAUTH3.crypto.core.pbkdf2 = deferCryptoCall("pbkdf2");
webCrypto.pbkdf2 = function (password, salt) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey'])
.then(function (key) {
@ -56,15 +32,12 @@
});
};
OAUTH3.crypto.core.encrypt = deferCryptoCall("encrypt");
webCrypto.encrypt = function (rawKey, iv, data) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt'])
.then(function (key) {
return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data);
});
};
OAUTH3.crypto.core.decrypt = deferCryptoCall("decrypt");
webCrypto.decrypt = function (rawKey, iv, data) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt'])
.then(function (key) {
@ -72,7 +45,6 @@
});
};
OAUTH3.crypto.core.genEcdsaKeyPair = deferCryptoCall("genEcdsaKeyPair");
webCrypto.genEcdsaKeyPair = function () {
return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(function (keyPair) {
@ -85,7 +57,6 @@
});
};
OAUTH3.crypto.core.sign = deferCryptoCall("sign");
webCrypto.sign = function (jwk, msg) {
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign'])
.then(function (key) {
@ -95,8 +66,6 @@
return new Uint8Array(sig);
});
};
OAUTH3.crypto.core.verify = deferCryptoCall("verify");
webCrypto.verify = function (jwk, msg, signature) {
// If the JWK has properties that should only exist on the private key or is missing
// "verify" in the key_ops, importing in as a public key won't work.
@ -113,7 +82,6 @@
};
function checkWebCrypto() {
/* global OAUTH3_crypto_fallback */
var loadFallback = function() {
var prom;
loadFallback = function () { return prom; };
@ -128,25 +96,25 @@
resolve();
}
};
script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js';
script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js';
body.appendChild(script);
});
return prom;
};
function checkException(name, func) {
return OAUTH3.PromiseA.resolve().then(func)
new OAUTH3.PromiseA(function (resolve) { resolve(func()); })
.then(function () {
OAUTH3.crypto.core[name] = webCrypto[name];
}, function (err) {
})
.catch(function (err) {
console.warn('error with WebCrypto', name, '- using fallback', err);
return loadFallback().then(function () {
loadFallback().then(function () {
OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
});
});
}
function checkResult(name, expected, func) {
finishBeforeReady.push(checkException(name, function () {
checkException(name, function () {
return func()
.then(function (result) {
if (typeof expected === typeof result) {
@ -159,7 +127,7 @@
throw new Error("result ("+result+") doesn't match expectation ("+expected+")");
}
});
}));
});
}
var zeroBuf = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
@ -191,19 +159,11 @@
return webCrypto.verify(jwk, dataBuf, sig);
});
// The results of these functions are less predictable, so we can't check their return value.
finishBeforeReady.push(checkException('genEcdsaKeyPair', function () {
checkException('genEcdsaKeyPair', function () {
return webCrypto.genEcdsaKeyPair();
}));
finishBeforeReady.push(checkException('sign', function () {
return webCrypto.sign(jwk, dataBuf);
}));
OAUTH3.PromiseA.all(finishBeforeReady)
.then(function(results) {
OAUTH3.crypto.core.ready = true;
deferedCalls.forEach(function(request) {
request();
});
checkException('sign', function () {
return webCrypto.sign(jwk, dataBuf);
});
}
checkWebCrypto();
@ -235,60 +195,100 @@
.then(OAUTH3._base64.bufferToUrlSafe);
};
OAUTH3.crypto.createKeyPair = function () {
// TODO: maybe support other types of key pairs, not just ECDSA P-256
return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) {
OAUTH3.crypto._createKey = function (ppid) {
var saltProm = OAUTH3.crypto.core.randomBytes(16);
var kekProm = saltProm.then(function (salt) {
return OAUTH3.crypto.core.pbkdf2(ppid, salt);
});
var ecdsaProm = OAUTH3.crypto.core.genEcdsaKeyPair()
.then(function (keyPair) {
return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
return keyPair;
});
});
};
OAUTH3.crypto.encryptKeyPair = function (keyPair, password) {
var saltProm = OAUTH3.crypto.core.randomBytes(16);
var kekProm = saltProm.then(function (salt) {
return OAUTH3.crypto.core.pbkdf2(password, salt);
});
return OAUTH3.PromiseA.all([
kekProm
, ecdsaProm
, saltProm
, OAUTH3.crypto.core.randomBytes(16)
, OAUTH3.crypto.core.randomBytes(12)
, ]).then(function (results) {
, OAUTH3.crypto.core.randomBytes(12)
]).then(function (results) {
var kek = results[0];
var salt = results[1];
var ecdsaIv = results[2];
var keyPair = results[1];
var salt = results[2];
var userSecret = results[3];
var ecdsaIv = results[4];
var secretIv = results[5];
var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey));
return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) {
return OAUTH3.PromiseA.all([
OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)))
, OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret)
])
.then(function (encrypted) {
return {
publicKey: keyPair.publicKey
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted)
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0])
, userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1])
, salt: OAUTH3._base64.bufferToUrlSafe(salt)
, ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
, };
, secretIv: OAUTH3._base64.bufferToUrlSafe(secretIv)
};
});
});
};
OAUTH3.crypto.decryptKeyPair = function (storedObj, password) {
OAUTH3.crypto._decryptKey = function (ppid, storedObj) {
var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt);
var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);
return OAUTH3.crypto.core.pbkdf2(password, salt)
return OAUTH3.crypto.core.pbkdf2(ppid, salt)
.then(function (key) {
return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
})
.then(OAUTH3._binStr.bufferToBinStr)
.then(JSON.parse)
.then(function (privateKey) {
return {
privateKey: privateKey
, publicKey: storedObj.publicKey
, };
.then(JSON.parse);
};
OAUTH3.crypto._getKey = function (ppid) {
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid))
.then(function (hash) {
var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash);
var promise;
if (window.localStorage.getItem(name) === null) {
promise = OAUTH3.crypto._createKey(ppid).then(function (key) {
window.localStorage.setItem(name, JSON.stringify(key));
return key;
});
} else {
promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name)));
}
return promise.then(function (storedObj) {
return OAUTH3.crypto._decryptKey(ppid, storedObj);
});
});
};
OAUTH3.crypto._signPayload = function (payload) {
return OAUTH3.crypto._getKey('some PPID').then(function (key) {
var header = {type: 'JWT', alg: key.alg, kid: key.kid};
var input = [
OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null))
, OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null))
].join('.');
return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input))
.then(OAUTH3._base64.bufferToUrlSafe)
.then(function (signature) {
return input + '.' + signature;
});
});
};

View File

@ -3,6 +3,39 @@
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
OAUTH3.query.parse = function (search) {
// parse a query or a hash
if (-1 !== ['#', '?'].indexOf(search[0])) {
search = search.substring(1);
}
// Solve for case of search within hash
// example: #/authorization_dialog/?state=...&redirect_uri=...
var queryIndex = search.indexOf('?');
if (-1 !== queryIndex) {
search = search.substr(queryIndex + 1);
}
var args = search.split('&');
var argsParsed = {};
var i, arg, kvp, key, value;
for (i = 0; i < args.length; i += 1) {
arg = args[i];
if (-1 === arg.indexOf('=')) {
argsParsed[decodeURIComponent(arg).trim()] = true;
}
else {
kvp = arg.split('=');
key = decodeURIComponent(kvp[0]).trim();
value = decodeURIComponent(kvp[1]).trim();
argsParsed[key] = value;
}
}
return argsParsed;
};
OAUTH3.scope.parse = function (scope) {
return (scope||'').split(/[, ]/g);
};
OAUTH3.url.parse = function (url) {
// TODO browser
// Node should replace this
@ -25,16 +58,8 @@ OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) {
};
OAUTH3.url.checkRedirect = function (client, query) {
console.warn("[security] URL path checking not yet implemented");
if (!query) {
query = client;
client = query.client_uri;
}
client = client.url || client;
// it doesn't matter who the referrer is as long as the destination
// is an authorized destination for the client in question
// (though it may not hurt to pass the referrer's info on to the client)
var clientUrl = OAUTH3.url.normalize(client);
var clientUrl = OAUTH3.url.normalize(client.url);
var redirectUrl = OAUTH3.url.normalize(query.redirect_uri);
// General rule:
@ -47,18 +72,6 @@ OAUTH3.url.checkRedirect = function (client, query) {
return true;
}
var callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK?'+OAUTH3.query.stringify({
'redirect_uri': redirectUrl
, 'allowed_urls': clientUrl
, 'client_id': client
, 'referrer_uri': OAUTH3.uri.normalize(window.document.referrer)
});
if (query.debug) {
console.log('Redirect Attack');
console.log(query);
window.alert("DEBUG MODE checkRedirect error encountered. Check the console.");
}
location.href = callbackUrl;
return false;
};
OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) {
@ -97,11 +110,13 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
// Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps)
//
// POST https://example.com/api/issuer@oauth3.org/access_token
// POST https://example.com/api/org.oauth3.provider/access_token
// { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var type = 'access_token';
var grantType = 'password';
if (!opts.password) {
if (opts.otp) {
@ -110,13 +125,16 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
}
}
var args = directive.access_token;
var scope = opts.scope || directive.authn_scope;
var clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
var clientUri = opts.client_uri;
var args = directive[type];
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
// TODO require user agent
var params = {
client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri
, grant_type: 'password'
, grant_type: grantType
, username: opts.username
, password: opts.password || otpCode || undefined
, totp: opts.totp || opts.totpToken || opts.totp_token || undefined
@ -131,21 +149,23 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
//, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (opts.totp) {
params.totp = opts.totp;
}
if (opts.client_uri) {
params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
if (!params.clientAgreeTos) {
if (clientUri) {
params.clientAgreeTos = clientAgreeTos;
if (!clientAgreeTos) {
throw new Error('Developer Error: missing clientAgreeTos uri');
}
}
var scope = opts.scope || directive.authn_scope;
if (scope) {
params.scope = OAUTH3.scope.stringify(scope);
}
var uri = args.url;
var body;
if ('GET' === args.method.toUpperCase()) {
uri += '?' + OAUTH3.query.stringify(params);
} else {
@ -161,10 +181,6 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
OAUTH3.urls.grants = function (directive, opts) {
// directive = { issuer, authorization_decision }
// opts = { response_type, scopes{ granted, requested, pending, accepted } }
var grantsDir = directive.grants;
if (!grantsDir) {
throw new Error("provider doesn't support grants");
}
if (!opts) {
throw new Error("You must supply a directive and an options object.");
@ -179,35 +195,35 @@ OAUTH3.urls.grants = function (directive, opts) {
console.warn("You should supply options.referrer");
}
if (!opts.method) {
console.warn("You should supply options.method as either 'GET', or 'POST'");
opts.method = grantsDir.method || 'GET';
console.warn("You must supply options.method as either 'GET', or 'POST'");
}
if ('POST' === opts.method) {
if ('string' !== typeof opts.scope) {
throw new Error("You must supply options.scope as a comma-delimited string of scopes");
console.warn("You should supply options.scope as a space-delimited string of scopes");
}
if ('string' !== typeof opts.sub) {
console.log("provide 'sub' to urls.grants to specify the PPID for the client");
if (-1 === ['token', 'code'].indexOf(opts.response_type)) {
throw new Error("You must supply options.response_type as 'token' or 'code'");
}
}
var url = OAUTH3.url.resolve(directive.api, grantsDir.url)
var url = OAUTH3.url.resolve(directive.api, directive.grants.url)
.replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri))
.replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED')
.replace(/(:azp|:client_id)/g, !opts.all && OAUTH3.uri.normalize(opts.client_id || opts.client_uri) || '')
.replace(/\/\/$/, '/') // if there's a double slash due to the sub not existing
;
var data = {
client_id: opts.client_id
, client_uri: opts.client_uri
, referrer: opts.referrer
, response_type: opts.response_type
, scope: opts.scope
, sub: opts.sub
, tenant_id: opts.tenant_id
};
var body;
if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(data);
} else {
}
else {
body = data;
}
@ -218,96 +234,21 @@ OAUTH3.urls.grants = function (directive, opts) {
, session: opts.session
};
};
//OAUTH3.urls.accessToken = function (directive, opts)
OAUTH3.urls.clientToken = function (directive, opts) {
var tokenDir = directive.access_token;
if (!tokenDir) {
throw new Error("provider doesn't support getting access tokens");
}
if (!opts) {
throw new Error("You must supply a directive and an options object.");
}
if (!(opts.azp || opts.client_id)) {
throw new Error("You must supply options.client_id.");
}
if (!opts.session) {
throw new Error("You must supply options.session.");
}
if (!opts.method) {
opts.method = tokenDir.method || 'POST';
}
var params = {
grant_type: 'issuer_token'
, client_id: opts.azp || opts.client_id
, azp: opts.azp || opts.client_id
, aud: opts.aud
, exp: opts.exp
, refresh_token: opts.refresh_token
, refresh_exp: opts.refresh_exp
};
var url = OAUTH3.url.resolve(directive.api, tokenDir.url);
var body;
if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(params);
} else {
body = params;
}
return {
method: opts.method
, url: url
, data: body
, session: opts.session
};
};
OAUTH3.urls.publishKey = function (directive, opts) {
var jwkDir = directive.publish_jwk;
if (!jwkDir) {
throw new Error("provider doesn't support publishing public keys");
}
if (!opts) {
throw new Error("You must supply a directive and an options object.");
}
if (!opts.session) {
throw new Error("You must supply 'options.session'.");
}
if (!(opts.public_key || opts.publicKey)) {
throw new Error("You must supply 'options.public_key'.");
}
var url = OAUTH3.url.resolve(directive.api, jwkDir.url)
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
;
return {
method: jwkDir.method || opts.method || 'POST'
, url: url
, data: opts.public_key || opts.publicKey
, session: opts.session
};
};
OAUTH3.urls.credentialMeta = function (directive, opts) {
return OAUTH3.url.resolve(directive.api, directive.credential_meta.url)
.replace(':type', 'email')
.replace(':id', opts.email)
};
OAUTH3.authn = OAUTH3.authn || {};
OAUTH3.authn = {};
OAUTH3.authn.loginMeta = function (directive, opts) {
var url = OAUTH3.urls.credentialMeta(directive, opts);
return OAUTH3.request({
method: directive.credential_meta.method || 'GET'
// TODO lint urls
// TODO client_uri
, url: url
, url: OAUTH3.url.resolve(directive.api, directive.credential_meta.url)
.replace(':type', 'email')
.replace(':id', opts.email)
});
};
OAUTH3.urls.otp = function (directive, opts) {
OAUTH3.authn.otp = function (directive, opts) {
// TODO client_uri
return {
var preq = {
method: directive.credential_otp.method || 'POST'
, url: OAUTH3.url.resolve(directive.api, directive.credential_otp.url)
, data: {
@ -320,17 +261,20 @@ OAUTH3.urls.otp = function (directive, opts) {
, username: opts.email
}
};
};
OAUTH3.authn.otp = function (directive, opts) {
var preq = OAUTH3.urls.otp(directive, opts);
return OAUTH3.request(preq);
};
OAUTH3.authn.resourceOwnerPassword = function (directive, opts) {
var providerUri = directive.issuer;
return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) {
var data = resp.data;
//var scope = opts.scope;
//var appId = opts.appId;
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts);
// TODO return not the raw request?
return OAUTH3.request(prequest).then(function (req) {
var data = req.data;
data.provider_uri = providerUri;
if (data.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data));
@ -340,52 +284,20 @@ OAUTH3.authn.resourceOwnerPassword = function (directive, opts) {
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, data
);
}).then(function (session) {
if (!opts.rememberDevice && !opts.remember_device) {
return session;
}
return OAUTH3.PromiseA.resolve().then(function () {
if (!OAUTH3.crypto) {
throw new Error("OAuth3 crypto library unavailable");
}
return OAUTH3.crypto.createKeyPair().then(function (keyPair) {
return OAUTH3.request(OAUTH3.urls.publishKey(directive, {
session: session
, publicKey: keyPair.publicKey
})).then(function () {
return OAUTH3.hooks.keyPairs.set(session.token.sub, keyPair);
});
});
}).then(function () {
return session;
}, function (err) {
console.error('failed to save keys to remember device', err);
window.alert('Failed to remember device');
return session;
});
});
};
OAUTH3.authz = {};
OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
// OAuth3.requests.grants(providerUri, {}); // return list of grants
// OAuth3.checkGrants(providerUri, {}); //
var clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer);
var scope = clientParams.scope || 'authn@oauth3.org';
if ('authn@oauth3.org' === scope.toString()) {
// implicit ppid grant is automatic
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
// TODO check user preference if implicit ppid grant is allowed
//return generateToken(session, clientObj);
}
var scope = clientParams.scope || '';
var clientObj = clientParams;
return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) {
if (granted) {
if (typeof granted.scope === 'string') {
return OAUTH3.scope.parse(granted.scope);
} else if (Array.isArray(granted.scope)) {
return granted.scope;
}
if (!scope) {
scope = 'oauth3_authn';
}
return OAUTH3.authz.grants(providerUri, {
@ -393,31 +305,74 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
, client_id: clientUri
, client_uri: clientUri
, session: session
}).then(function (results) {
return results.grants;
}, function (err) {
if (!/no .*grants .*found/i.test(err.message)) {
throw err;
}).then(function (grantResults) {
var grants;
var grantedScopes;
var grantedScopesMap;
var pendingScopes;
var acceptedScopes;
var scopes = scope.split(/[+, ]/g);
var callbackUrl;
// it doesn't matter who the referrer is as long as the destination
// is an authorized destination for the client in question
// (though it may not hurt to pass the referrer's info on to the client)
if (!OAUTH3.url.checkRedirect(grantResults.client, clientObj)) {
callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK'
+ '?redirect_uri=' + clientObj.redirect_uri
+ '&allowed_urls=' + grantResults.client.url
+ '&client_id=' + clientUri
+ '&referrer_uri=' + OAUTH3.uri.normalize(window.document.referrer)
;
if (clientParams.debug) {
console.log('grantResults Redirect Attack');
console.log(grantResults);
console.log(clientObj);
window.alert("DEBUG MODE checkRedirect error encountered. Check the console.");
}
return [];
});
}).then(function (granted) {
var requested = OAUTH3.scope.parse(scope);
var accepted = [];
var pending = [];
requested.forEach(function (scp) {
if (granted.indexOf(scp) < 0) {
pending.push(scp);
} else {
accepted.push(scp);
location.href = callbackUrl;
return;
}
if ('oauth3_authn' === scope) {
// implicit ppid grant is automatic
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
// TODO check user preference if implicit ppid grant is allowed
//return generateToken(session, clientObj);
}
grants = (grantResults).grants.filter(function (grant) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
return true;
}
});
grantedScopesMap = {};
acceptedScopes = [];
pendingScopes = scopes.filter(function (requestedScope) {
return grants.every(function (grant) {
if (!grant.scope) {
grant.scope = 'oauth3_authn';
}
var gscopes = grant.scope.split(/[+, ]/g);
gscopes.forEach(function (s) { grantedScopesMap[s] = true; });
if (-1 !== gscopes.indexOf(requestedScope)) {
// already accepted in the past
acceptedScopes.push(requestedScope);
}
else {
// true, is pending
return true;
}
});
});
grantedScopes = Object.keys(grantedScopesMap);
return {
requested: requested // all requested, now
, granted: granted // all granted, ever
, accepted: accepted // intersection of granted (ever) and requested (now)
, pending: pending // not yet accepted
pending: pendingScopes // not yet accepted
, granted: grantedScopes // all granted, ever
, requested: scopes // all requested, now
, accepted: acceptedScopes // granted (ever) and requested (now)
};
});
};
@ -426,406 +381,156 @@ OAUTH3.authz.grants = function (providerUri, opts) {
client_id: providerUri
, debug: opts.debug
}).then(function (directive) {
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts);
}).then(function (grantsResult) {
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) {
if ('POST' === opts.method) {
// TODO this is clientToken
return grantsResult.originalData || grantsResult.data;
}
var grants = grantsResult.originalData || grantsResult.data;
// TODO
if (grants.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants));
}
// the responses for GET and POST requests are now the same, so we should alway be able to
// use the response and save it the same way.
if (opts.all || ('GET' !== opts.method && 'POST' !== opts.method)) {
return grants;
}
OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants);
OAUTH3.hooks.grants.set(opts.client_id + '-client', grants.client);
grants.grants.forEach(function (grant) {
var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId;
// TODO should save as an array
OAUTH3.hooks.grants.set(clientId, [ grant ]);
});
return {
client: grants.azp
, clientSub: grants.azpSub
, grants: OAUTH3.scope.parse(grants.scope)
client: OAUTH3.hooks.grants.get(opts.client_id + '-client')
, grants: OAUTH3.hooks.grants.get(opts.client_id) || []
};
});
});
};
function calcExpiration(exp, now) {
if (!exp) {
return;
}
if (typeof exp === 'string') {
var match = /^(\d+\.?\d*) *(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(exp);
if (!match) {
return now;
}
var num = parseFloat(match[1]);
var type = (match[2] || 's').toLowerCase()[0];
switch (type) {
case 'y': num *= 365.25; /* falls through */
case 'd': num *= 24; /* falls through */
case 'h': num *= 60; /* falls through */
case 'm': num *= 60; /* falls through */
case 's': exp = num;
}
}
if (typeof exp !== 'number') {
throw new Error('invalid expiration provided: '+exp);
}
now = now || Math.floor(Date.now() / 1000);
if (exp > now) {
return exp;
} else if (exp > 31557600) {
console.warn('tried to set expiration to more that a year');
exp = 31557600;
}
return now + exp;
}
OAUTH3.authz.redirectWithToken = function (providerUri, session, clientParams, scopes) {
if (!OAUTH3.url.checkRedirect(clientParams.client_uri, clientParams)) {
return;
}
if ('token' !== clientParams.response_type) {
var message;
if ('code' === clientParams.response_type) {
message = "Authorization Code Redirect NOT IMPLEMENTED";
} else {
message = "Authorization response type '"+clientParams.response_type+"' not supported";
}
window.alert(message);
throw new Error(message);
}
var prom;
if (scopes.new) {
prom = OAUTH3.authz.grants(providerUri, {
session: session
, method: 'POST'
, client_id: clientParams.client_uri
, referrer: clientParams.referrer
, scope: scopes.accepted.concat(scopes.new).join(',')
});
} else {
prom = OAUTH3.PromiseA.resolve();
}
scopes.new = scopes.new || [];
return prom.then(function () {
return OAUTH3.hooks.keyPairs.get(session.token.sub);
}).then(function (keyPair) {
if (!keyPair) {
return OAUTH3.discover(providerUri, {
client_id: providerUri
, debug: clientParams.debug
}).then(function (directive) {
return OAUTH3.request(OAUTH3.urls.clientToken(directive, {
if ('token' === clientParams.response_type) {
// get token and redirect client-side
return OAUTH3.authz.grants(providerUri, {
method: 'POST'
, session: session
, referrer: clientParams.referrer
, response_type: clientParams.response_type
, client_id: clientParams.client_uri
, azp: clientParams.client_uri
, aud: clientParams.aud
, exp: clientParams.exp
, refresh_token: clientParams.refresh_token
, refresh_exp: clientParams.refresh_exp
, client_uri: clientParams.client_uri
, scope: scopes.granted.concat(scopes.new).join(',')
, response_type: clientParams.response_type
, referrer: clientParams.referrer
, session: session
, subject: clientParams.subject
, debug: clientParams.debug
})).then(function (result) {
return result.originalData || result.data;
});
});
}
}).then(function (results) {
return OAUTH3.hooks.grants.get(keyPair.sub, clientParams.client_uri).then(function (grant) {
var now = Math.floor(Date.now()/1000);
var payload = {
iat: now
, iss: providerUri
, aud: clientParams.aud || providerUri
, azp: clientParams.client_uri
, sub: grant.azpSub
, scope: OAUTH3.scope.stringify(grant.scope)
, };
var signProms = [];
signProms.push(OAUTH3.jwt.sign(Object.assign({
exp: calcExpiration(clientParams.exp || '1h', now)
}, payload), keyPair));
// if (clientParams.refresh_token) {
signProms.push(OAUTH3.jwt.sign(Object.assign({
exp: calcExpiration(clientParams.refresh_exp, now)
}, payload), keyPair));
// }
return OAUTH3.PromiseA.all(signProms).then(function (tokens) {
console.log('created new tokens for client');
return {
access_token: tokens[0]
, refresh_token: tokens[1]
, scope: OAUTH3.scope.stringify(grant.scope)
, token_type: 'bearer'
};
});
});
}).then(function (session) {
// TODO limit refresh token to an expirable token
// TODO inform client not to persist token
OAUTH3.url.redirect(clientParams, scopes, session);
}, function (err) {
console.error('unexpected error creating client tokens', err);
OAUTH3.url.redirect(clientParams, scopes, {error: err});
/*
if (clientParams.dnsTxt) {
Object.keys(results).forEach(function (key) {
if (/refresh/.test(key)) {
results[key] = undefined;
}
});
}
*/
OAUTH3.url.redirect(clientParams, scopes, results);
});
}
else if ('code' === clientParams.response_type) {
// get token and redirect server-side
// (requires insecure form post as per spec)
//OAUTH3.requests.authorizationDecision();
window.alert("Authorization Code Redirect NOT IMPLEMENTED");
throw new Error("Authorization Code Redirect NOT IMPLEMENTED");
}
};
OAUTH3.requests = {};
//OAUTH3.accounts = {};
OAUTH3.requests.accounts = {};
OAUTH3.urls.accounts = {};
OAUTH3.urls.accounts._ = function (directives, directive, session, opts) {
opts = opts || {};
var dir = directive || {
//url: OAUTH3.url.normalize(directives.api) + '/api/issuer@oauth3.org/accounts/:accountId'
url: OAUTH3.url.normalize(directives.api) + '/api/issuer@oauth3.org/acl/profiles/:accountId'
//, method: 'GET'
OAUTH3.requests.accounts.update = function (directive, session, opts) {
var dir = directive.update_account || {
method: 'POST'
, url: OAUTH3.url.normalize(directive.api) + '/api/issuer@oauth3.org/accounts/:accountId'
, bearer: 'Bearer'
};
var url = dir.url
.replace(/:accountId/, opts.accountId || '')
.replace(/\/$/, '')
.replace(/:accountId/, opts.accountId)
;
return {
url: url
//, method: dir.method || 'POST'
, session: session
/*
return OAUTH3.request({
method: dir.method || 'POST'
, url: url
, headers: {
'Authorization': (dir.bearer || 'Bearer') + ' ' + (session.access_token || session.accessToken)
'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken
}
*/
};
};
OAUTH3.urls.accounts.get = function (directives, session) {
var urlObj = OAUTH3.urls.accounts._(directives, directives.account, session);
urlObj.method = (directives.account || { method: 'GET' }).method;
return urlObj;
};
OAUTH3.urls.accounts.update = function (directives, session, opts) {
var urlObj = OAUTH3.urls.accounts._(directives, directives.update_account, session, opts);
urlObj.method = (directives.update_account || { method: 'POST' }).method;
urlObj.json = {
, json: {
name: opts.name
, comment: opts.comment
, displayName: opts.displayName
, priority: opts.priority
};
return urlObj;
}
});
};
OAUTH3.urls.accounts.create = function (directives, session, account) {
var urlObj = OAUTH3.urls.accounts._(directives, directives.create_account, session);
var profile = {
OAUTH3.requests.accounts.create = function (directive, session, account) {
var dir = directive.create_account || {
method: 'POST'
, url: OAUTH3.url.normalize(directive.api) + '/api/issuer@oauth3.org/accounts'
, bearer: 'Bearer'
};
var data = {
// TODO fix the server to just use one scheme
// account = { nick, self: { comment, username } }
// account = { name, comment, display_name, priority }
account: {
nick: account.display_name
// "name" is unique and what would be reserved in a url {{name}}.issuer.org or issuer.org/users/{{name}}
, name: account.name
, comment: account.comment
, display_name: account.display_name
, priority: account.priority
, self: {
nick: account.display_name
, name: account.name
, comment: account.comment
, display_name: account.display_name
, priority: account.priority
}
}
, logins: [
{
token: session.access_token
}
]
};
var credentials = [ { token: session.access_token } ];
urlObj.method = (directives.create_account || { method: 'POST' }).method;
urlObj.json = {
// TODO fix the server to just use one scheme
// account = { nick, self: { comment, username } }
// account = { name, comment, display_name, priority }
credentials: credentials
, profile: profile
// 'account' is deprecated in favor of 'profile'
, account: profile
// 'logins' is deprecated in favor of 'credentials'
, logins: credentials
};
return urlObj;
};
OAUTH3.requests.accounts.get = function (directives, session) {
var urlObj = OAUTH3.urls.accounts.get(directives, session);
return OAUTH3.request(urlObj);
};
OAUTH3.requests.accounts.update = function (directives, session, opts) {
var urlObj = OAUTH3.urls.accounts.update(directives, session, opts);
return OAUTH3.request(urlObj);
};
OAUTH3.requests.accounts.create = function (directive, session, account) {
var urlObj = OAUTH3.urls.accounts.create(directives, session, account);
return OAUTH3.request(urlObj);
};
return OAUTH3.request({
method: dir.method || 'POST'
, url: dir.url
, session: session
, data: data
});
};
OAUTH3.hooks.grants = {
get: function (id, clientUri) {
OAUTH3.hooks._checkStorage('grants', 'get');
if (!id) {
throw new Error("id is not set");
// Provider Only
set: function (clientUri, newGrants) {
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
console.warn(newGrants);
if (!this._cache) { this._cache = {}; }
console.log('clientUri, newGrants');
console.log(clientUri, newGrants);
this._cache[clientUri] = newGrants;
return newGrants;
}
if (!clientUri) {
throw new Error("clientUri is not set");
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(id, OAUTH3.uri.normalize(clientUri)));
}
, set: function (id, clientUri, grants) {
OAUTH3.hooks._checkStorage('grants', 'set');
if (!id) {
throw new Error("id is not set");
}
if (!clientUri) {
throw new Error("clientUri is not set");
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(id, OAUTH3.uri.normalize(clientUri), grants));
}
, all: function () {
OAUTH3.hooks._checkStorage('grants', 'all');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.all());
}
, clear: function () {
OAUTH3.hooks._checkStorage('grants', 'clear');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.clear());
}
};
OAUTH3.hooks.keyPairs = {
get: function (id) {
OAUTH3.hooks._checkStorage('keyPairs', 'get');
if (!id) {
throw new Error("id is not set");
}
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.get(id));
}
, set: function (id, keyPair) {
OAUTH3.hooks._checkStorage('keyPairs', 'set');
if (!keyPair && id.privateKey && id.publicKey && id.sub) {
keyPair = id;
id = keyPair.sub;
}
if (!keyPair) {
return OAUTH3.PromiseA.reject(new Error("no key pair provided to save"));
}
if (!id) {
throw new Error("id is not set");
}
keyPair.sub = keyPair.sub || id;
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.set(id, keyPair));
}
, all: function () {
OAUTH3.hooks._checkStorage('keyPairs', 'all');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.all());
}
, clear: function () {
OAUTH3.hooks._checkStorage('keyPairs', 'clear');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.clear());
}
};
OAUTH3.hooks.session.get = function (providerUri, id) {
OAUTH3.hooks._checkStorage('sessions', 'get');
var sessProm = OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id));
if (providerUri !== OAUTH3.clientUri(window.location)) {
return sessProm;
}
return sessProm.then(function (session) {
if (session && OAUTH3.jwt.freshness(session.token) === 'fresh') {
return session;
}
return OAUTH3.hooks.keyPairs.all().then(function (keyPairs) {
var pair;
if (id) {
pair = keyPairs[id];
} else if (Object.keys(keyPairs).length === 1) {
id = Object.keys(keyPairs)[0];
pair = keyPairs[id];
} else if (Object.keys(keyPairs).length > 1) {
console.error("too many users, don't know which key to use");
}
if (!pair) {
// even if the access token isn't fresh, the session might have a refresh token
return session;
}
var now = Math.floor(Date.now()/1000);
var payload = {
iat: now
, iss: providerUri
, aud: providerUri
, azp: providerUri
, sub: pair.sub || id
, scope: ''
, exp: now + 3600
};
return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) {
console.log('created new token for provider');
return OAUTH3.hooks.session.refresh(
{ provider_uri: providerUri, client_uri: providerUri || providerUri }
, { access_token: token }
);
});
});
});
};
OAUTH3._defaultStorage.grants = {
prefix: 'grants-'
, get: function (id, clientUri) {
var key = this.prefix + id+'/'+clientUri;
var result = JSON.parse(window.localStorage.getItem(key) || 'null');
return OAUTH3.PromiseA.resolve(result);
}
, set: function (id, clientUri, grants) {
var key = this.prefix + id+'/'+clientUri;
window.localStorage.setItem(key, JSON.stringify(grants));
return this.get(clientUri);
}
, all: function () {
var prefix = this.prefix;
var result = {};
OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) {
var split = key.replace(prefix, '').split('/');
if (!result[split[0]]) { result[split[0]] = {}; }
result[split[0]][split[1]] = JSON.parse(window.localStorage.getItem(key) || 'null');
});
return OAUTH3.PromiseA.resolve(result);
}
, clear: function () {
OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) {
window.localStorage.removeItem(key);
});
return OAUTH3.PromiseA.resolve();
}
};
OAUTH3._defaultStorage.keyPairs = {
prefix: 'key_pairs-'
, get: function (id) {
var result = JSON.parse(window.localStorage.getItem(this.prefix + id) || 'null');
return OAUTH3.PromiseA.resolve(result);
}
, set: function (id, keyPair) {
window.localStorage.setItem(this.prefix + id, JSON.stringify(keyPair));
return this.get(id);
}
, all: function () {
var prefix = this.prefix;
var result = {};
OAUTH3._defaultStorage._getStorageKeys(prefix, window.localStorage).forEach(function (key) {
result[key.replace(prefix, '')] = JSON.parse(window.localStorage.getItem(key) || 'null');
});
return OAUTH3.PromiseA.resolve(result);
}
, clear: function () {
OAUTH3._defaultStorage._getStorageKeys(this.prefix, window.localStorage).forEach(function (key) {
window.localStorage.removeItem(key);
});
return OAUTH3.PromiseA.resolve();
, get: function (clientUri) {
clientUri = OAUTH3.uri.normalize(clientUri);
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
if (!this._cache) { this._cache = {}; }
console.log('clientUri, existingGrants');
console.log(clientUri, this._cache[clientUri]);
return this._cache[clientUri];
}
};

View File

@ -27,9 +27,9 @@
OAUTH3.authz.scopes = function () {
return OAUTH3.PromiseA.resolve({
pending: [ 'authn@oauth3.org' ] // not yet accepted
pending: ['oauth3_authn'] // not yet accepted
, granted: [] // all granted, ever
, requested: [ 'authn@oauth3.org' ] // all requested, now
, requested: ['oauth3_authn'] // all requested, now
, accepted: [] // granted (ever) and requested (now)
});
};

View File

@ -28,7 +28,6 @@ OAUTH3._base64.atob = function (base64) {
OAUTH3._base64.btoa = function (text) {
return new Buffer(text, 'utf8').toString('base64');
};
OAUTH3._defaultStorage = require('./oauth3.node.storage');
OAUTH3._node = {};
OAUTH3._node.discover = function(providerUri/*, opts*/) {
@ -44,7 +43,6 @@ OAUTH3._node.request = function(preq/*, _sys*/) {
method: preq.method
, url: preq.url || preq.uri
, headers: preq.headers
, timeout: preq.timeout || undefined
, json: preq.data || preq.body || preq.json || undefined // TODO which to use?
, formData: preq.formData || undefined
};

View File

@ -67,9 +67,10 @@ module.exports = {
, sessions: {
all: function (providerUri) {
return fs.readdirAsync(sessionsdir).then(function (nodes) {
var dirname = path.join(oauth3dir, 'sessions');
return fs.readdirAsync(dirname).then(function (nodes) {
return nodes.map(function (node) {
var result = require(path.join(sessionsdir, node));
var result = require(path.join(dirname, node));
if (result.link) {
return null;
}
@ -90,7 +91,7 @@ module.exports = {
result = require(path.join(sessionsdir, providerUri + '.json'));
// TODO make safer
if (result.link && '/' !== result.link[0] && !/\.\./.test(result.link)) {
result = require(path.join(sessionsdir, result.link));
result = require(path.join(oauth3dir, 'sessions', result.link));
}
}
} catch(e) {
@ -112,9 +113,10 @@ module.exports = {
});
}
, clear: function () {
return fs.readdirAsync(sessionsdir).then(function (nodes) {
var dirname = path.join(oauth3dir, 'sessions');
return fs.readdirAsync(dirname).then(function (nodes) {
return PromiseA.all(nodes.map(function (node) {
return fs.unlinkAsync(path.join(sessionsdir, node));
return fs.unlinkAsync(path.join(dirname, node));
}));
});
}

View File

@ -9,7 +9,7 @@ OAUTH3.api['tunnel.token'] = function (providerUri, opts) {
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/tunnel@oauth3.org/accounts/' + session.token.sub + '/token'
+ '/api/org.oauth3.tunnel/accounts/' + session.token.sub + '/token'
, session: session
, data: {
domains: opts.data.domains

View File

@ -1,6 +1,6 @@
{
"name": "oauth3.js",
"version": "1.2.2",
"version": "1.0.10",
"description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.",
"main": "oauth3.node.js",
"scripts": {
@ -9,7 +9,7 @@
},
"repository": {
"type": "git",
"url": "git@git.oauth3.org:OAuth3/oauth3.js.git"
"url": "git@git.daplie.com:OAuth3/oauth3.js.git"
},
"keywords": [
"oauth",
@ -35,7 +35,7 @@
"bluebird": "^3.5.0",
"elliptic": "^6.4.0",
"request": "^2.81.0",
"terminal-forms.js": "git+https://git.oauth3.org/OAuth3/terminal-forms.js.git#v1"
"terminal-forms.js": "git+https://git.daplie.com/OAuth3/terminal-forms.js.git#v1"
},
"devDependencies": {
"browserify-aes": "^1.0.6",
@ -49,6 +49,6 @@
"gulp-uglify": "^2.1.0",
"vinyl-source-stream": "^1.1.0"
},
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"author": "AJ ONeal <aj@daplie.com> (https://daplie.com/)",
"license": "(MIT OR Apache-2.0)"
}

560
prefactor/oauth3.browser.js Normal file
View File

@ -0,0 +1,560 @@
;(function (exports) {
'use strict';
var OAUTH3 = exports.OAUTH3;
var OAUTH3_CORE = exports.OAUTH3_CORE;
function getDefaultAppUrl() {
console.warn('[deprecated] using window.location.{protocol, host, pathname} when opts.client_id should be used');
return window.location.protocol
+ '//' + window.location.host
+ (window.location.pathname).replace(/\/?$/, '')
;
}
var browser = exports.OAUTH3_BROWSER = {
window: window
, clientUri: function (location) {
return OAUTH3_CORE.normalizeUri(location.host + location.pathname);
}
, discover: function (providerUri, opts) {
if (!providerUri) {
throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
}
var directives = OAUTH3.hooks.getDirectives(providerUri);
if (directives && directives.issuer) {
return OAUTH3.PromiseA.resolve(directives);
}
return browser._discoverHelper(providerUri, opts).then(function (directives) {
directives.issuer = directives.issuer || OAUTH3_CORE.normalizeUrl(providerUri);
console.log('discoverHelper', directives);
return OAUTH3.hooks.setDirectives(providerUri, directives);
});
}
, _discoverHelper: function (providerUri, opts) {
opts = opts || {};
//opts.debug = true;
providerUri = OAUTH3_CORE.normalizeUrl(providerUri);
if (window.location.hostname.match(providerUri)) {
console.warn("It looks like you're a provider checking for your own directive,"
+ " so we we're just gonna use OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })");
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.core.normalizeUrl(providerUri) + '/.well-known/oauth3/directives.json'
});
}
if (!window.location.hostname.match(opts.client_id || opts.client_uri)) {
console.warn("It looks like your client_id doesn't match your current window... this probably won't end well");
console.warn(opts.client_id || opts.client_uri, window.location.hostname);
}
var discObj = OAUTH3_CORE.urls.discover(providerUri, { client_id: (opts.client_id || opts.client_uri || getDefaultAppUrl()), debug: opts.debug });
// TODO ability to reuse iframe instead of closing
return browser.insertIframe(discObj.url, discObj.state, opts).then(function (params) {
if (params.error) {
return OAUTH3_CORE.formatError(providerUri, params.error);
}
var directives = JSON.parse(atob(OAUTH3_CORE.utils.urlSafeBase64ToBase64(params.result || params.directives)));
return directives;
}, function (err) {
return OAUTH3.PromiseA.reject(err);
});
}
, discoverAuthorizationDialog: function(providerUri, opts) {
var discObj = OAUTH3.core.discover(providerUri, opts);
// hmm... we're gonna need a broker for this since switching windows is distracting,
// popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
// eventually it should be the browser (and postMessage may be a viable option now), but whatever...
// TODO allow postMessage from providerUri in addition to callback
var discWin = OAUTH3.openWindow(discObj.url, discObj.state, { reuseWindow: 'conquerer' });
return discWin.then(function (params) {
console.log('discwin params');
console.log(params);
// discWin.child
// TODO params should have response_type indicating json, binary, etc
var directives = JSON.parse(atob(OAUTH3.core.utils.urlSafeBase64ToBase64(params.result || params.directives)));
console.log('directives');
console.log(directives);
// Do some stuff
var authObj = OAUTH3.core.implicitGrant(
directives
, { redirect_uri: opts.redirect_uri
, debug: opts.debug
, client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri || opts.client_id
}
);
if (params.debug) {
window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!");
}
return new OAUTH3.PromiseA(function (resolve, reject) {
// TODO check if authObj.url is relative or full
discWin.child.location = OAUTH3.core.urls.resolve(providerUri, authObj.url);
if (params.debug) {
discWin.child.focus();
}
window['--oauth3-callback-' + authObj.state] = function (tokens) {
if (tokens.error) {
return reject(OAUTH3.core.formatError(tokens.error));
}
if (params.debug || tokens.debug) {
if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) {
discWin.child.close();
}
}
else {
discWin.child.close();
}
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) {
var promise;
if (!opts.windowType) {
opts.windowType = 'popup';
}
if ('background' === opts.windowType) {
promise = browser.insertIframe(url, state, opts);
} else if ('popup' === opts.windowType) {
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 {
throw new Error("login framing method options.windowType not specified or not type yet implemented");
}
return promise.then(function (params) {
var err;
if (params.error || params.error_description) {
err = new Error(params.error_description || "Unknown response error");
err.code = params.error || "E_UKNOWN_ERROR";
err.params = params;
return OAUTH3.PromiseA.reject(err);
}
return params;
});
}
, insertIframe: function (url, state, opts) {
opts = opts || {};
if (opts.debug) {
opts.timeout = opts.timeout || 15 * 60 * 1000;
}
var promise = new OAUTH3.PromiseA(function (resolve, reject) {
var tok;
var iframeDiv;
function cleanup() {
delete window['--oauth3-callback-' + state];
iframeDiv.remove();
clearTimeout(tok);
tok = null;
}
window['--oauth3-callback-' + state] = function (params) {
resolve(params);
cleanup();
};
tok = setTimeout(function () {
var err = new Error("the iframe request did not complete within 15 seconds");
err.code = "E_TIMEOUT";
reject(err);
cleanup();
}, opts.timeout || 15 * 1000);
// TODO hidden / non-hidden (via directive even)
var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" ';
if (opts.debug) {
framesrc += ' width="800px" height="800px" style="opacity: 0.8;" frameborder="1"';
}
else {
framesrc += ' width="1px" height="1px" frameborder="0"';
}
framesrc += '></iframe>';
iframeDiv = window.document.createElement('div');
iframeDiv.innerHTML = framesrc;
window.document.body.appendChild(iframeDiv);
});
// TODO periodically garbage collect expired handlers from window object
return promise;
}
, openWindow: function (url, state, opts) {
if (opts.debug) {
opts.timeout = opts.timeout || 15 * 60 * 1000;
}
var promise = new OAUTH3.PromiseA(function (resolve, reject) {
var tok;
function cleanup() {
clearTimeout(tok);
tok = null;
delete window['--oauth3-callback-' + state];
// this is last in case the window self-closes synchronously
// (should never happen, but that's a negotiable implementation detail)
if (!opts.reuseWindow) {
promise.child.close();
}
}
window['--oauth3-callback-' + state] = function (params) {
console.log('YOLO!!');
resolve(params);
cleanup();
};
tok = setTimeout(function () {
var err = new Error("the windowed request did not complete within 3 minutes");
err.code = "E_TIMEOUT";
reject(err);
cleanup();
}, opts.timeout || 3 * 60 * 1000);
setTimeout(function () {
if (!promise.child) {
reject("TODO: open the iframe first and discover oauth3 directives before popup");
cleanup();
}
}, 0);
});
// TODO allow size changes (via directive even)
promise.child = window.open(
url
, 'oauth3-login-' + (opts.reuseWindow || state)
, 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620)
);
// TODO periodically garbage collect expired handlers from window object
return promise;
}
//
// Logins
//
, authn: {
authorizationRedirect: function (providerUri, opts) {
// TODO get own directives
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3_CORE.urls.authorizationRedirect(
directive
, opts
);
if (!prequest.state) {
throw new Error("[Devolper Error] [authorization redirect] prequest.state is empty");
}
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) {
// TODO let broker=true change behavior to open discover inline with frameRequest
// TODO OAuth3 provider should use the redirect URI as the appId?
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3_CORE.urls.implicitGrant(
directive
// TODO OAuth3 provider should referrer / referer / origin as the appId?
, opts
);
if (!prequest.state) {
throw new Error("[Devolper Error] [implicit grant] prequest.state is empty");
}
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) {
opts = opts || {};
return OAUTH3.discover(providerUri, opts).then(function (directive) {
var prequest = OAUTH3_CORE.urls.logout(
directive
, opts
);
// Oauth3.init({ logout: function () {} });
//return Oauth3.logout();
var redirectUri = opts.redirect_uri || opts.redirectUri
|| (window.location.protocol + '//' + (window.location.host + window.location.pathname) + 'oauth3.html')
;
var params = {
// logout=true for all logins/accounts
// logout=app-scoped-login-id for a single login
action: 'logout'
// TODO specify specific accounts / logins to delete from session
, accounts: true
, logins: true
, redirect_uri: redirectUri
, state: prequest.state
, debug: opts.debug
};
if (prequest.url === params.redirect_uri) {
return OAUTH3.PromiseA.resolve();
}
prequest.url += '#' + OAUTH3_CORE.querystringify(params);
return OAUTH3.insertIframe(prequest.url, prequest.state, opts);
});
}
}
, isIframe: function isIframe () {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
, parseUrl: function (url) {
var parser = document.createElement('a');
parser.href = url;
return parser;
}
, isRedirectHostSafe: function (referrerUrl, redirectUrl) {
var src = browser.parseUrl(referrerUrl);
var dst = browser.parseUrl(redirectUrl);
// TODO how should we handle subdomains?
// It should be safe for api.example.com to redirect to example.com
// But it may not be safe for to example.com to redirect to aj.example.com
// It is also probably not safe for sally.example.com to redirect to john.example.com
// The client should have a list of allowed URLs to choose from and perhaps a wildcard will do
//
// api.example.com.evil.com SHOULD NOT match example.com
return dst.hostname === src.hostname;
}
, checkRedirect: function (client, query) {
console.warn("[security] URL path checking not yet implemented");
var clientUrl = OAUTH3.core.normalizeUrl(client.url);
var redirectUrl = OAUTH3.core.normalizeUrl(query.redirect_uri);
// General rule:
// I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain)
// but not a longer (more subs) or different domain or a longer path (on the same domain)
// We can callback to an explicitly listed domain (TODO and path)
if (browser.isRedirectHostSafe(clientUrl, redirectUrl)) {
return true;
}
return false;
}
/*
, redirect: function (redirect) {
if (parser.search) {
parser.search += '&';
} else {
parser.search += '?';
}
parser.search += 'error=E_NO_SESSION';
redirectUri = parser.href;
window.location.href = redirectUri;
}
*/
, hackFormSubmit: function (opts) {
opts = opts || {};
scope.authorizationDecisionUri = DaplieApiConfig.providerUri + '/api/org.oauth3.provider/authorization_decision';
scope.updateScope();
var redirectUri = scope.appQuery.redirect_uri.replace(/^https?:\/\//i, 'https://');
var separator;
// TODO check that we appropriately use '#' for implicit and '?' for code
// (server-side) in an OAuth2 backwards-compatible way
if ('token' === scope.appQuery.response_type) {
separator = '#';
}
else if ('code' === scope.appQuery.response_type) {
separator = '?';
}
else {
separator = '#';
}
if (scope.pendingScope.length && !opts.allow) {
redirectUri += separator + Oauth3.querystringify({
error: 'access_denied'
, error_description: 'None of the permissions were accepted'
, error_uri: 'https://oauth3.org/docs/errors#access_denied'
, state: scope.appQuery.state
});
window.location.href = redirectUri;
return;
}
// TODO move to Oauth3? or not?
// this could be implementation-specific,
// but it may still be nice to provide it as de-facto
var url = DaplieApiConfig.apiBaseUri + '/api/org.oauth3.provider/grants/:client_id/:account_id'
.replace(/:client_id/g, scope.appQuery.client_id || scope.appQuery.client_uri)
.replace(/:account_id/g, scope.selectedAccountId)
;
var account = scope.sessionAccount;
var session = { accessToken: account.token, refreshToken: account.refreshToken };
var preq = {
url: url
, method: 'POST'
, data: {
scope: updateAccepted()
, response_type: scope.appQuery.response_type
, referrer: document.referrer || document.referer || ''
, referer: document.referrer || document.referer || ''
, tenant_id: scope.appQuery.tenant_id
, client_id: scope.appQuery.client_id
, client_uri: scope.appQuery.client_uri
}
, session: session
};
preq.clientId = preq.appId = DaplieApiConfig.appId || DaplieApiConfig.clientId;
preq.clientUri = preq.appUri = DaplieApiConfig.appUri || DaplieApiConfig.clientUri;
// TODO need a way to have middleware in Oauth3.request for TherapySession et al
return Oauth3.request(preq).then(function (resp) {
var err;
var data = resp.data || {};
if (data.error) {
err = new Error(data.error.message || data.errorDescription);
err.message = data.error.message || data.errorDescription;
err.code = resp.data.error.code || resp.data.error;
err.uri = 'https://oauth3.org/docs/errors#' + (resp.data.error.code || resp.data.error);
return $q.reject(err);
}
if (!(data.code || data.accessToken)) {
err = new Error("No grant code");
return $q.reject(err);
}
return data;
}).then(function (data) {
redirectUri += separator + Oauth3.querystringify({
state: scope.appQuery.state
, code: data.code
, access_token: data.access_token
, expires_at: data.expires_at
, expires_in: data.expires_in
, scope: data.scope
, refresh_token: data.refresh_token
, refresh_expires_at: data.refresh_expires_at
, refresh_expires_in: data.refresh_expires_in
});
if ('token' === scope.appQuery.response_type) {
window.location.href = redirectUri;
return;
}
else if ('code' === scope.appQuery.response_type) {
scope.hackFormSubmitHelper(redirectUri);
return;
}
else {
console.warn("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'");
console.warn(redirectUri);
throw new Error("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'");
}
}, function (err) {
redirectUri += separator + Oauth3.querystringify({
error: err.code || 'server_error'
, error_description: err.message || "Server Error: It's not your fault"
, error_uri: err.uri || 'https://oauth3.org/docs/errors#server_error'
, state: scope.appQuery.state
});
console.error('Grant Code Error NOT IMPLEMENTED');
console.error(err);
console.error(redirectUri);
//window.location.href = redirectUri;
});
}
, hackFormSubmitHelper: function (uri) {
// TODO de-jQuerify
//window.location.href = redirectUri;
//return;
// the only way to do a POST that redirects the current window
window.jQuery('form.js-hack-hidden-form').attr('action', uri);
// give time for the apply to take place
window.setTimeout(function () {
window.jQuery('form.js-hack-hidden-form').submit();
}, 50);
}
};
browser.requests = browser.authn;
Object.keys(browser).forEach(function (key) {
if ('requests' === key) {
Object.keys(browser.requests).forEach(function (key) {
OAUTH3.requests[key] = browser.requests[key];
});
return;
}
OAUTH3[key] = browser[key];
});
}('undefined' !== typeof exports ? exports : window));

42
prefactor/oauth3.cache.js Normal file
View File

@ -0,0 +1,42 @@
oauth3.discover = function (providerUri, opts) {
opts = opts || {};
console.log('DEBUG oauth3.discover', providerUri);
console.log(opts);
if (opts.directives) {
return oauth3.PromiseA.resolve(opts.directives);
}
var promise;
var promise2;
var directives;
var updatedAt;
var fresh;
providerUri = oauth3.normalizeUrl(providerUri);
try {
directives = JSON.parse(localStorage.getItem('oauth3.' + providerUri + '.directives'));
console.log('DEBUG oauth3.discover cache', directives);
updatedAt = localStorage.getItem('oauth3.' + providerUri + '.directives.updated_at');
console.log('DEBUG oauth3.discover updatedAt', updatedAt);
updatedAt = new Date(updatedAt).valueOf();
console.log('DEBUG oauth3.discover updatedAt', updatedAt);
} catch(e) {
// ignore
}
fresh = (Date.now() - updatedAt) < (24 * 60 * 60 * 1000);
if (directives) {
promise = oauth3.PromiseA.resolve(directives);
if (fresh) {
//console.log('[local] [fresh directives]', directives);
return promise;
}
}
promise2 = oauth3._discoverHelper(providerUri, opts);
return promise || promise2;
};

473
prefactor/oauth3.core.js Normal file
View File

@ -0,0 +1,473 @@
;(function (exports) {
'use strict';
// NOTE: we assume that directive.provider_uri exists
var core = {};
core.urls = core;
function getDefaultAppApiBase() {
console.warn('[deprecated] using window.location.host when opts.appApiBase should be used');
return 'https://' + window.location.host;
}
core.parsescope = function (scope) {
return (scope||'').split(/[+, ]/g);
};
core.stringifyscope = function (scope) {
if (Array.isArray(scope)) {
scope = scope.join(' ');
}
return scope;
};
core.querystringify = function (params) {
var qs = [];
Object.keys(params).forEach(function (key) {
// TODO nullify instead?
if ('undefined' === typeof params[key]) {
return;
}
if ('scope' === key) {
params[key] = core.stringifyscope(params[key]);
}
qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
});
return qs.join('&');
};
// Modified from http://stackoverflow.com/a/7826782
core.queryparse = function (search) {
// parse a query or a hash
if (-1 !== ['#', '?'].indexOf(search[0])) {
search = search.substring(1);
}
// Solve for case of search within hash
// example: #/authorization_dialog/?state=...&redirect_uri=...
var queryIndex = search.indexOf('?');
if (-1 !== queryIndex) {
search = search.substr(queryIndex + 1);
}
var args = search.split('&');
var argsParsed = {};
var i, arg, kvp, key, value;
for (i = 0; i < args.length; i += 1) {
arg = args[i];
if (-1 === arg.indexOf('=')) {
argsParsed[decodeURIComponent(arg).trim()] = true;
}
else {
kvp = arg.split('=');
key = decodeURIComponent(kvp[0]).trim();
value = decodeURIComponent(kvp[1]).trim();
argsParsed[key] = value;
}
}
return argsParsed;
};
core.formatError = function (providerUri, params) {
var err = new Error(params.error_description || params.error.message || "Unknown error when discoving provider '" + providerUri + "'");
err.uri = params.error_uri || params.error.uri;
err.code = params.error.code || params.error;
return err;
};
core.normalizePath = function (path) {
return path.replace(/^\//, '').replace(/\/$/, '');
};
core.normalizeUri = function (providerUri) {
// tested with
// example.com
// example.com/
// http://example.com
// https://example.com/
return providerUri
.replace(/^(https?:\/\/)?/i, '')
.replace(/\/?$/, '')
;
};
core.normalizeUrl = function (providerUri) {
// tested with
// example.com
// example.com/
// http://example.com
// https://example.com/
return providerUri
.replace(/^(https?:\/\/)?/i, 'https://')
.replace(/\/?$/, '')
;
};
// these might not really belong in core... not sure
// there should be node.js- and browser-specific versions probably
core.utils = {
urlSafeBase64ToBase64: function (b64) {
// URL-safe Base64 to Base64
// https://en.wikipedia.org/wiki/Base64
// https://gist.github.com/catwell/3046205
var mod = b64.length % 4;
if (2 === mod) { b64 += '=='; }
if (3 === mod) { b64 += '='; }
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
return b64;
}
, base64ToUrlSafeBase64: function (b64) {
// Base64 to URL-safe Base64
b64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
b64 = b64.replace(/=+/g, '');
return b64;
}
, randomState: function () {
var i;
var ch;
var str;
// TODO put in different file for browser vs node
try {
return Array.prototype.slice.call(window.crypto.getRandomValues(new Uint8Array(16))).map(function (ch) { return (ch).toString(16); }).join('');
} catch(e) {
// TODO use fisher-yates on 0..255 and select [0] 16 times
// [security] https://medium.com/@betable/tifu-by-using-math-random-f1c308c4fd9d#.5qx0bf95a
// https://github.com/v8/v8/blob/b0e4dce6091a8777bda80d962df76525dc6c5ea9/src/js/math.js#L135-L144
// Note: newer versions of v8 do not have this bug, but other engines may still
console.warn('[security] crypto.getRandomValues() failed, falling back to Math.random()');
str = '';
for (i = 0; i < 32; i += 1) {
ch = Math.round(Math.random() * 255).toString(16);
if (ch.length < 2) { ch = '0' + ch; }
str += ch;
}
return str;
}
}
};
core.jwt = {
// decode only (no verification)
decode: function (str) {
// 'abc.qrs.xyz'
// [ 'abc', 'qrs', 'xyz' ]
// [ {}, {}, 'foo' ]
// { header: {}, payload: {}, signature: }
var parts = str.split(/\./g);
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
var atob = exports.atob || require('atob');
var b64 = core.utils.urlSafeBase64ToBase64(urlsafe64);
return atob(b64);
});
return {
header: JSON.parse(jsons[0])
, payload: JSON.parse(jsons[1])
, signature: parts[2] // should remain url-safe base64
};
}
, getFreshness: function (tokenMeta, staletime, now) {
staletime = staletime || (15 * 60);
now = now || Date.now();
var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000));
if (fresh >= staletime) {
return 'fresh';
}
if (fresh <= 0) {
return 'expired';
}
return 'stale';
}
// encode-only (no signature)
, encode: function (parts) {
parts.header = parts.header || { alg: 'none', typ: 'jwt' };
parts.signature = parts.signature || '';
var btoa = exports.btoa || require('btoa');
var result = [
core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.header, null)))
, core.utils.base64ToUrlSafeBase64(btoa(JSON.stringify(parts.payload, null)))
, parts.signature // should already be url-safe base64
].join('.');
return result;
}
};
core.urls.discover = function (providerUri, opts) {
if (!providerUri) {
throw new Error("cannot discover without providerUri");
}
if (!opts.client_id) {
throw new Error("cannot discover without options.client_id");
}
var clientId = core.normalizeUrl(opts.client_id || opts.client_uri);
providerUri = core.normalizeUrl(providerUri);
var params = {
action: 'directives'
, state: core.utils.randomState()
, redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html')
, response_type: 'rpc'
, _method: 'GET'
, _pathname: '.well-known/oauth3/directives.json'
, debug: opts.debug || undefined
};
var result = {
url: providerUri + '/.well-known/oauth3/#/?' + core.querystringify(params)
, state: params.state
, method: 'GET'
, query: params
};
return result;
};
core.urls.authorizationCode = function (/*directive, scope, redirectUri, clientId*/) {
//
// Example Authorization Code Request
// (not for use in the browser)
//
// GET https://example.com/api/org.oauth3.provider/authorization_dialog
// ?response_type=code
// &scope=`encodeURIComponent('profile.login profile.email')`
// &state=`cryptoutil.random().toString('hex')`
// &client_id=xxxxxxxxxxx
// &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
//
// NOTE: `redirect_uri` itself may also contain URI-encoded components
//
// NOTE: This probably shouldn't be done in the browser because the server
// needs to initiate the state. If it is done in a browser, the browser
// should probably request 'state' from the server beforehand
//
throw new Error("not implemented");
};
core.urls.authorizationRedirect = function (directive, opts) {
//console.log('[authorizationRedirect]');
//
// Example Authorization Redirect - from Browser to Consumer API
// (for generating a session securely on your own server)
//
// i.e. GET https://<<CONSUMER>>.com/api/org.oauth3.consumer/authorization_redirect/<<PROVIDER>>.com
//
// GET https://myapp.com/api/org.oauth3.consumer/authorization_redirect/`encodeURIComponent('example.com')`
// &scope=`encodeURIComponent('profile.login profile.email')`
//
// (optional)
// &state=`cryptoutil.random().toString('hex')`
// &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
//
// NOTE: This is not a request sent to the provider, but rather a request sent to the
// consumer (your own API) which then sets some state and redirects.
// This will initiate the `authorization_code` request on your server
//
opts = opts || {};
var scope = opts.scope || directive.authn_scope;
var providerUri = directive.provider_uri;
var params = {
state: core.utils.randomState()
, debug: opts.debug || undefined
};
var slimProviderUri = encodeURIComponent(providerUri.replace(/^(https?|spdy):\/\//, ''));
var authorizationRedirect = opts.authorizationRedirect;
if (scope) {
params.scope = scope;
}
if (opts.redirectUri) {
// this is really only for debugging
params.redirect_uri = opts.redirectUri;
}
// Note: the type check is necessary because we allow 'true'
// as an automatic mechanism when it isn't necessary to specify
if ('string' !== typeof authorizationRedirect) {
// TODO oauth3.json for self?
authorizationRedirect = (opts.appApiBase || getDefaultAppApiBase())
+ '/api/org.oauth3.consumer/authorization_redirect/:provider_uri';
}
authorizationRedirect = authorizationRedirect
.replace(/!(provider_uri)/, slimProviderUri)
.replace(/:provider_uri/, slimProviderUri)
.replace(/#{provider_uri}/, slimProviderUri)
.replace(/{{provider_uri}}/, slimProviderUri)
;
return {
url: authorizationRedirect + '?' + core.querystringify(params)
, method: 'GET'
, state: params.state // this becomes browser_state
, params: params // includes scope, final redirect_uri?
};
};
core.urls.implicitGrant = function (directive, opts) {
//console.log('[implicitGrant]');
//
// Example Implicit Grant Request
// (for generating a browser-only session, not a session on your server)
//
// GET https://example.com/api/org.oauth3.provider/authorization_dialog
// ?response_type=token
// &scope=`encodeURIComponent('profile.login profile.email')`
// &state=`cryptoutil.random().toString('hex')`
// &client_id=xxxxxxxxxxx
// &redirect_uri=`encodeURIComponent('https://myapp.com/oauth3.html')`
//
// NOTE: `redirect_uri` itself may also contain URI-encoded components
//
opts = opts || {};
var type = 'authorization_dialog';
var responseType = 'token';
var redirectUri = opts.redirect_uri;
var scope = opts.scope || directive.authn_scope;
var args = directive[type];
var uri = args.url;
var state = core.utils.randomState();
var params = {
debug: opts.debug || undefined
, client_uri: opts.client_uri || opts.clientUri || undefined
, client_id: opts.client_id || opts.client_uri || undefined
};
var result;
params.state = state;
params.response_type = responseType;
if (scope) {
params.scope = core.stringifyscope(scope);
}
if (!redirectUri) {
// TODO consider making this optional
console.error('missing redirect_uri');
}
params.redirect_uri = redirectUri;
uri += '?' + core.querystringify(params);
result = {
url: uri
, state: state
, method: args.method
, query: params
};
return result;
};
core.urls.resolve = function (base, next) {
if (/^https:\/\//i.test(next)) {
return next;
}
return core.normalizeUrl(base) + '/' + core.normalizePath(next);
};
core.urls.refreshToken = function (directive, opts) {
// grant_type=refresh_token
// Example Refresh Token Request
// (generally for 1st or 3rd party server-side, mobile, and desktop apps)
//
// POST https://example.com/api/oauth3/access_token
// { "grant_type": "refresh_token", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var type = 'access_token';
var grantType = 'refresh_token';
var scope = opts.scope || directive.authn_scope;
var clientSecret = opts.appSecret || opts.clientSecret;
var args = directive[type];
var params = {
"grant_type": grantType
, "refresh_token": opts.refresh_token || opts.refreshToken || (opts.session && opts.session.refresh_token)
, "response_type": 'token'
, "client_id": opts.appId || opts.app_id || opts.client_id || opts.clientId || opts.client_id || opts.clientId
, "client_uri": opts.client_uri || opts.clientUri
//, "scope": undefined
//, "client_secret": undefined
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
// TODO not allowed in the browser
if (clientSecret) {
params.client_secret = clientSecret;
}
if (scope) {
params.scope = core.stringifyscope(scope);
}
if ('GET' === args.method.toUpperCase()) {
uri += '?' + core.querystringify(params);
} else {
body = params;
}
return {
url: uri
, method: args.method
, data: body
};
};
core.urls.logout = function (directive, opts) {
opts = opts || {};
var type = 'logout';
var clientId = opts.appId || opts.clientId || opts.client_id;
var args = directive[type];
var params = {
client_id: opts.clientUri || opts.client_uri
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (opts.clientUri) {
params.client_uri = opts.clientUri;
}
if (clientId) {
params.client_id = clientId;
}
args.method = (args.method || 'GET').toUpperCase();
if ('GET' === args.method) {
uri += '?' + core.querystringify(params);
} else {
body = params;
}
return {
url: uri
, method: args.method || 'GET'
, data: body
};
};
exports.OAUTH3 = exports.OAUTH3 || { core: core };
exports.OAUTH3_CORE = core.OAUTH3_CORE = core;
if ('undefined' !== typeof module) {
module.exports = core;
}
}('undefined' !== typeof exports ? exports : window));

View File

@ -0,0 +1,302 @@
;(function (exports) {
'use strict';
var core = window.OAUTH3_CORE;
// Provider-Only
core.urls.loginCode = function (directive, opts) {
//
// Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps)
//
// POST https://api.example.com/api/org.oauth3.provider/otp
// { "request_otp": true, "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>" }
//
opts = opts || {};
var clientId = opts.appId || opts.clientId;
var args = directive.credential_otp;
if (!directive.credential_otp) {
console.log('[debug] loginCode directive:');
console.log(directive);
}
var params = {
"username": opts.id || opts.username
, "request_otp": true // opts.requestOtp || undefined
//, "jwt": opts.jwt // TODO sign a proof
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (opts.clientUri) {
params.client_uri = opts.clientUri;
}
if (opts.clientAgreeTos) {
params.client_agree_tos = opts.clientAgreeTos;
}
if (clientId) {
params.client_id = clientId;
}
if ('GET' === args.method.toUpperCase()) {
uri += '?' + core.querystringify(params);
} else {
body = params;
}
return {
url: uri
, method: args.method
, data: body
};
};
core.urls.resourceOwnerPassword = function (directive, opts) {
//
// Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps)
//
// POST https://example.com/api/org.oauth3.provider/access_token
// { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" }
//
opts = opts || {};
var type = 'access_token';
var grantType = 'password';
if (!opts.password) {
if (opts.otp) {
// for backwards compat
opts.password = opts.otp; // 'otp:' + opts.otpUuid + ':' + opts.otp;
}
}
var scope = opts.scope || directive.authn_scope;
var clientId = opts.appId || opts.clientId || opts.client_id;
var clientAgreeTos = opts.clientAgreeTos || opts.client_agree_tos;
var clientUri = opts.clientUri || opts.client_uri || opts.clientUrl || opts.client_url;
var args = directive[type];
var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
var params = {
"grant_type": grantType
, "username": opts.username
, "password": opts.password || otpCode || undefined
, "totp": opts.totp || opts.totpToken || opts.totp_token || undefined
, "otp": otpCode
, "password_type": otpCode && 'otp'
, "otp_code": otpCode
, "otp_uuid": opts.otpUuid || opts.otp_uuid || undefined
, "user_agent": opts.userAgent || opts.useragent || opts.user_agent || undefined // AJ's Macbook
, "jwk": (opts.rememberDevice || opts.remember_device) && opts.jwk || undefined
//, "public_key": opts.rememberDevice && opts.publicKey || undefined
//, "public_key_type": opts.rememberDevice && opts.publicKeyType || undefined // RSA/ECDSA
//, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key
, debug: opts.debug || undefined
};
var uri = args.url;
var body;
if (opts.totp) {
params.totp = opts.totp;
}
if (clientId) {
params.clientId = clientId;
}
if (clientUri) {
params.clientUri = clientUri;
params.clientAgreeTos = clientAgreeTos;
if (!clientAgreeTos) {
throw new Error('Developer Error: missing clientAgreeTos uri');
}
}
if (scope) {
params.scope = core.stringifyscope(scope);
}
if ('GET' === args.method.toUpperCase()) {
uri += '?' + core.querystringify(params);
} else {
body = params;
}
return {
url: uri
, method: args.method
, data: body
};
};
core.urls.grants = function (directive, opts) {
// directive = { issuer, authorization_decision }
// opts = { response_type, scopes{ granted, requested, pending, accepted } }
if (!opts) {
throw new Error("You must supply a directive and an options object.");
}
if (!opts.client_id) {
throw new Error("You must supply options.client_id.");
}
if (!opts.session) {
throw new Error("You must supply options.session.");
}
if (!opts.referrer) {
console.warn("You should supply options.referrer");
}
if (!opts.method) {
console.warn("You must supply options.method as either 'GET', or 'POST'");
}
if ('POST' === opts.method) {
if ('string' !== typeof opts.scope) {
console.warn("You should supply options.scope as a space-delimited string of scopes");
}
if (-1 === ['token', 'code'].indexOf(opts.response_type)) {
throw new Error("You must supply options.response_type as 'token' or 'code'");
}
}
var url = core.urls.resolve(directive.issuer, directive.grants.url)
.replace(/(:azp|:client_id)/g, core.normalizeUri(opts.client_id || opts.client_uri))
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
;
var data = {
client_id: opts.client_id
, client_uri: opts.client_uri
, referrer: opts.referrer
, response_type: opts.response_type
, scope: opts.scope
, tenant_id: opts.tenant_id
};
var body;
if ('GET' === opts.method) {
url += '?' + core.querystringify(data);
}
else {
body = data;
}
return {
method: opts.method
, url: url
, data: body
, session: opts.session
};
};
core.urls.authorizationDecision = function (directive, opts) {
var url = core.urls.resolve(directive.issuer, directive.authorization_decision.url);
if (!opts) {
throw new Error("You must supply a directive and an options object");
}
console.info(url);
throw new Error("NOT IMPLEMENTED authorization_decision");
};
core.authz = core.authz || {};
core.authz.scopes = function (session, clientParams) {
// OAuth3.requests.grants(providerUri, {}); // return list of grants
// OAuth3.checkGrants(providerUri, {}); //
var clientUri = OAUTH3.core.normalizeUri(clientParams.client_uri || window.document.referrer);
var scope = clientParams.scope || '';
var clientObj = clientParams;
if (!scope) {
scope = 'oauth3_authn';
}
//$('.js-user-avatar').attr('src', userAvatar);
/*
console.log('grants options');
console.log(loc.hash);
console.log(loc.search);
console.log(clientObj);
console.log(session.token);
console.log(window.document.referrer);
*/
return OAUTH3.requests.grants(CONFIG.host, {
method: 'GET'
, client_id: clientUri
, client_uri: clientUri
, session: session
}).then(function (grantResults) {
var grants;
var grantedScopes;
var grantedScopesMap;
var pendingScopes;
var acceptedScopes;
var scopes = scope.split(/[+, ]/g);
var callbackUrl;
console.log('previous grants:');
console.log(grantResults);
if (grantResults.data.error) {
window.alert('grantResults: ' + grantResults.data.error_description || grantResults.data.error.message);
return;
}
// it doesn't matter who the referrer is as long as the destination
// is an authorized destination for the client in question
// (though it may not hurt to pass the referrer's info on to the client)
if (!OAUTH3.checkRedirect(grantResults.data.client, clientObj)) {
callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK'
+ '?redirect_uri=' + clientObj.redirect_uri
+ '&allowed_urls=' + grantResults.data.client.url
+ '&client_id=' + clientUri
+ '&referrer_uri=' + OAUTH3.core.normalizeUri(window.document.referrer)
;
location.href = callbackUrl;
return;
}
if ('oauth3_authn' === scope) {
// implicit ppid grant is automatic
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
// TODO check user preference if implicit ppid grant is allowed
//return generateToken(session, clientObj);
}
grants = (grantResults.originalData||grantResults.data).grants.filter(function (grant) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
return true;
}
});
grantedScopesMap = {};
acceptedScopes = [];
pendingScopes = scopes.filter(function (requestedScope) {
return grants.every(function (grant) {
if (!grant.scope) {
grant.scope = 'oauth3_authn';
}
var gscopes = grant.scope.split(/[+, ]/g);
gscopes.forEach(function (s) { grantedScopesMap[s] = true; });
if (-1 !== gscopes.indexOf(requestedScope)) {
// already accepted in the past
acceptedScopes.push(requestedScope);
}
else {
// true, is pending
return true;
}
});
});
grantedScopes = Object.keys(grantedScopesMap);
return {
pending: pendingScopes // not yet accepted
, granted: grantedScopes // all granted, ever
, requested: scopes // all requested, now
, accepted: acceptedScopes // granted (ever) and requested (now)
};
});
};
exports.OAUTH3_CORE_PROVIDER = core;
if ('undefined' !== typeof module) {
module.exports = core;
}
}('undefined' !== typeof exports ? exports : window));

109
prefactor/oauth3.jquery.js Normal file
View File

@ -0,0 +1,109 @@
(function () {
'use strict';
// I did try to shim jQuery's deferred, but it's just too clunky.
// Here I use es6-promise which lacks asynchrity, but it's the smallest Promise implementation.
// Only Opera Mini and MSIE (even on 11) will use this shim, so no biggie;
var oauth3 = window.OAUTH3;
var count = 0;
function inject() {
count += 1;
if (count >= 100) {
throw new Error("you forgot to include rsvp.js, methinks");
}
/*
*
[window.Promise, window.ES6Promise, window.RSVP.Promise].forEach(function (PromiseA) {
var x = 1; new PromiseA(function (resolve, reject) { console.log('x', 1 === x); resolve(); }); x = 2; void null;
var y = 1; PromiseA.resolve().then(function () { console.log('y', 2 === x); }); y = 2; void null;
});
*/
var PromiseA = /*(window.RSVP && window.RSVP.Promise) || window.ES6Promise || */window.Promise;
if ('undefined' !== typeof PromiseA) {
oauth3.providePromise(PromiseA).then(function () {
// ignore
window.jqOauth3 = oauth3;
}, function (err) {
console.error(err);
console.error("Bad Promise Implementation!");
});
return;
}
// because MSIE can't tell when a script is loaded
setTimeout(inject, 100);
}
if ('undefined' === typeof Promise) {
// support Opera Mini and MSIE 11+ (which doesn't support <!-- [if IE]> detection)
/* jshint ignore: start */
document.write('<script src="bower_components/es6-promise/promise.min.js"></script>');
/* jshint ignore: end */
/*
// I would have used this, but it turns out that
// MSIE can't tell when a script has loaded
var js = document.createElement("script");
js.setAttribute("src", "bower_components/es6-promise/promise.js");
js.setAttribute("type", "text/javascript");
document.getElementsByTagName("head")[0].appendChild(js);
*/
}
inject();
function Request(opts) {
if (!opts.method) {
throw new Error("Developer Error: you must set method as one of 'GET', 'POST', 'DELETE', etc");
}
var req = {
url: opts.url
// Noted: jQuery 1.9 finally added 'method' as an alias of 'type'
, method: opts.method
// leaving type for backwards compat
, type: opts.method
, headers: opts.headers || {}
};
// don't allow accidetal querystring via 'data'
if (opts.data && !/get|delete/i.test(opts.method)) {
req.data = opts.data;
if (opts.data && 'object' === typeof opts.data) {
req.data = JSON.stringify(req.data);
req.headers['Content-Type'] = 'application/json; charset=utf-8';
}
}
// I don't trust jQuery promises...
return new oauth3.PromiseA(function (resolve, reject) {
$.ajax(req).then(function (data, textStatus, jqXhr) {
var resp = {};
Object.keys(jqXhr).forEach(function (key) {
// particularly we have to get rid of .then
if ('function' !== typeof jqXhr[key]) {
resp[key] = jqXhr[key];
}
});
resp.data = data;
resp.status = textStatus;
resp.request = jqXhr;
resolve(resp);
}, function (jqXhr, textStatus, errorThrown) {
errorThrown.request = jqXhr;
errorThrown.response = jqXhr;
errorThrown.status = textStatus;
reject(errorThrown);
});
});
}
oauth3.provideRequest(Request);
}());

445
prefactor/oauth3.js Normal file
View File

@ -0,0 +1,445 @@
/* global Promise */
(function (exports) {
'use strict';
var oauth3 = {};
var core = exports.OAUTH3_CORE || require('./oauth3.core.js');
oauth3.requests = {};
if ('undefined' !== typeof Promise) {
oauth3.PromiseA = Promise;
} else {
console.warn("[oauth3.js] Remember to call oauth3.providePromise(Promise) with a proper Promise implementation");
}
oauth3.providePromise = function (PromiseA) {
oauth3.PromiseA = PromiseA;
if (oauth3._testPromise) {
return oauth3._testPromise(PromiseA).then(function () {
oauth3.PromiseA = PromiseA;
});
}
oauth3.PromiseA = PromiseA;
return PromiseA.resolve();
};
// TODO move recase out
/*
oauth3._recaseRequest = function (recase, req) {
// convert JavaScript camelCase to oauth3/ruby snake_case
if (req.data && 'object' === typeof req.data) {
req.originalData = req.data;
req.data = recase.snakeCopy(req.data);
}
return req;
};
oauth3._recaseResponse = function (recase, resp) {
// convert oauth3/ruby snake_case to JavaScript camelCase
if (resp.data && 'object' === typeof resp.data) {
resp.originalData = resp.data;
resp.data = recase.camelCopy(resp.data);
}
return resp;
};
*/
oauth3.hooks = {
checkSession: function (preq, opts) {
if (!preq.session) {
console.warn('[oauth3.hooks.checkSession] no session');
return oauth3.PromiseA.resolve(null);
}
var freshness = oauth3.core.jwt.getFreshness(preq.session.token, opts.staletime);
console.info('[oauth3.hooks.checkSession] freshness', freshness, preq.session);
switch (freshness) {
case 'stale':
return oauth3.hooks.sessionStale(preq.session);
case 'expired':
return oauth3.hooks.sessionExpired(preq.session).then(function (newSession) {
preq.session = newSession;
return newSession;
});
//case 'fresh':
default:
return oauth3.PromiseA.resolve(preq.session);
}
}
, sessionStale: function (staleSession) {
console.info('[oauth3.hooks.sessionStale] called');
if (oauth3.hooks._stalePromise) {
return oauth3.PromiseA.resolve(staleSession);
}
oauth3.hooks._stalePromise = oauth3.requests.refreshToken(
staleSession.provider_uri
, { client_uri: staleSession.client_uri
, session: staleSession
, debug: staleSession.debug
}
).then(function (newSession) {
oauth3.hooks._stalePromise = null;
return newSession; // oauth3.hooks.refreshSession(staleSession, newSession);
}, function () {
oauth3.hooks._stalePromise = null;
});
return oauth3.PromiseA.resolve(staleSession);
}
, sessionExpired: function (expiredSession) {
console.info('[oauth3.hooks.sessionExpired] called');
return oauth3.requests.refreshToken(
expiredSession.provider_uri
, { client_uri: expiredSession.client_uri
, session: expiredSession
, debug: expiredSession.debug
}
).then(function (newSession) {
return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession);
});
}
, refreshSession: function (oldSession, newSession) {
var providerUri = oldSession.provider_uri;
var clientUri = oldSession.client_uri;
console.info('[oauth3.hooks.refreshSession] oldSession', JSON.parse(JSON.stringify(oldSession)));
console.info('[oauth3.hooks.refreshSession] newSession', newSession);
// shim for account create which does not return new refresh_token
newSession.refresh_token = newSession.refresh_token || oldSession.refresh_token;
Object.keys(oldSession).forEach(function (key) {
oldSession[key] = undefined;
});
Object.keys(newSession).forEach(function (key) {
oldSession[key] = newSession[key];
});
// info about the session of this API call
oldSession.provider_uri = providerUri; // aud
oldSession.client_uri = clientUri; // azp
// info about the newly-discovered token
oldSession.token = oldSession.meta = core.jwt.decode(oldSession.access_token).payload;
oldSession.token.sub = oldSession.token.sub
|| (oldSession.token.acx && oldSession.token.acx.id)
|| (oldSession.token.axs && oldSession.token.axs.length && oldSession.token.axs[0].appScopedId)
;
oldSession.token.client_uri = clientUri;
oldSession.token.provider_uri = providerUri;
if (!oldSession.token.sub) {
// TODO this is broken hard
console.warn('TODO implementation for OAUTH3.hooks.accounts.create (GUI, CLI, or API)');
}
if (oldSession.refresh_token) {
oldSession.refresh = core.jwt.decode(oldSession.refresh_token).payload;
oldSession.refresh.sub = oldSession.refresh.sub
|| (oldSession.refresh.acx && oldSession.refresh.acx.id)
|| (oldSession.refresh.axs && oldSession.refresh.axs.length && oldSession.refresh.axs[0].appScopedId)
;
oldSession.refresh.provider_uri = providerUri;
}
console.info('[oauth3.hooks.refreshSession] refreshedSession', oldSession);
// set for a set of audiences
return oauth3.PromiseA.resolve(oauth3.hooks.setSession(providerUri, oldSession));
}
, setSession: function (providerUri, newSession) {
if (!providerUri) {
console.error(new Error('no providerUri').stack);
}
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[ERROR] Please implement OAUTH3.hooks.setSession = function (providerUri, newSession) { return newSession; }');
console.warn(newSession);
if (!oauth3.hooks._sessions) { oauth3.hooks._sessions = {}; }
oauth3.hooks._sessions[providerUri] = newSession;
return newSession;
}
, getSession: function (providerUri) {
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[ERROR] Please implement OAUTH3.hooks.getSession = function (providerUri) { return savedSession; }');
if (!oauth3.hooks._sessions) { oauth3.hooks._sessions = {}; }
return oauth3.hooks._sessions[providerUri];
}
, setDirectives: function (providerUri, directives) {
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[oauth3.hooks.setDirectives] PLEASE IMPLEMENT -- Your Fault');
console.warn(directives);
if (!oauth3.hooks._directives) { oauth3.hooks._directives = {}; }
window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives));
oauth3.hooks._directives[providerUri] = directives;
return directives;
}
, getDirectives: function (providerUri) {
providerUri = oauth3.core.normalizeUri(providerUri);
console.warn('[oauth3.hooks.getDirectives] PLEASE IMPLEMENT -- Your Fault');
if (!oauth3.hooks._directives) { oauth3.hooks._directives = {}; }
return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}');
//return oauth3.hooks._directives[providerUri];
}
// Provider Only
, setGrants: function (clientUri, newGrants) {
clientUri = oauth3.core.normalizeUri(clientUri);
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
console.warn(newGrants);
if (!oauth3.hooks._grants) { oauth3.hooks._grants = {}; }
console.log('clientUri, newGrants');
console.log(clientUri, newGrants);
oauth3.hooks._grants[clientUri] = newGrants;
return newGrants;
}
, getGrants: function (clientUri) {
clientUri = oauth3.core.normalizeUri(clientUri);
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
if (!oauth3.hooks._grants) { oauth3.hooks._grants = {}; }
console.log('clientUri, existingGrants');
console.log(clientUri, oauth3.hooks._grants[clientUri]);
return oauth3.hooks._grants[clientUri];
}
};
// TODO simplify (nix recase)
oauth3.provideRequest = function (rawRequest, opts) {
opts = opts || {};
//var Recase = exports.Recase || require('recase');
// TODO make insensitive to providing exceptions
//var recase = Recase.create({ exceptions: {} });
function lintAndRequest(preq) {
function goGetHer() {
if (preq.session) {
// 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)");
preq.headers = preq.headers || {};
preq.headers.Authorization = 'Bearer ' + preq.session.access_token;
}
if (!oauth3._lintRequest) {
return rawRequest(preq);
}
return oauth3._lintRequest(preq, opts).then(function (preq) {
return rawRequest(preq);
});
}
if (!preq.session) {
return goGetHer();
}
console.warn('lintAndRequest checkSession', preq);
return oauth3.hooks.checkSession(preq, opts).then(goGetHer);
}
if (opts.rawCase) {
oauth3.request = lintAndRequest;
return;
}
// Wrap oauth3 api calls in snake_case / camelCase conversion
oauth3.request = function (req, opts) {
//console.log('[D] [oauth3 req.url]', req.url);
opts = opts || {};
if (opts.rawCase) {
return lintAndRequest(req, opts);
}
//req = oauth3._recaseRequest(recase, req);
return lintAndRequest(req, opts).then(function (res) {
//return oauth3._recaseResponse(recase, res);
return res;
});
};
/*
return oauth3._testRequest(request).then(function () {
oauth3.request = request;
});
*/
};
// TODO merge with regular token access point and new response_type=federated ?
oauth3.requests.clientToken = function (providerUri, opts) {
return oauth3.discover(providerUri, opts).then(function (directive) {
return oauth3.request(core.urls.grants(directive, opts)).then(function (grantsResult) {
return grantsResult.originalData || grantsResult.data;
});
});
};
oauth3.requests.grants = function (providerUri, opts) {
return oauth3.discover(providerUri, {
client_id: providerUri
, debug: opts.debug
}).then(function (directive) {
return oauth3.request(core.urls.grants(directive, opts)).then(function (grantsResult) {
if ('POST' === opts.method) {
// TODO this is clientToken
return grantsResult.originalData || grantsResult.data;
}
var grants = grantsResult.originalData || grantsResult.data;
// TODO
if (grants.error) {
return oauth3.PromiseA.reject(oauth3.core.formatError(grants.error));
}
console.warn('requests.grants', grants);
oauth3.hooks.setGrants(opts.client_id + '-client', grants.client);
grants.grants.forEach(function (grant) {
var clientId = grant.client_id || grant.oauth_client_id || grant.oauthClientId;
// TODO should save as an array
oauth3.hooks.setGrants(clientId, [ grant ]);
});
return {
client: oauth3.hooks.getGrants(opts.client_id + '-client')
, grants: oauth3.hooks.getGrants(opts.client_id) || []
};
});
});
};
oauth3.requests.loginCode = function (providerUri, opts) {
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.loginCode(directive, opts);
return oauth3.request(prequest).then(function (res) {
// result = { uuid, expires_at }
return {
otpUuid: res.data.uuid
, otpExpires: res.data.expires_at
};
});
});
};
oauth3.loginCode = oauth3.requests.loginCode;
oauth3.requests.resourceOwnerPassword = function (providerUri, opts) {
//var scope = opts.scope;
//var appId = opts.appId;
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.resourceOwnerPassword(directive, opts);
return oauth3.request(prequest).then(function (req) {
var data = (req.originalData || req.data);
data.provider_uri = providerUri;
if (data.error) {
return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data.error));
}
return oauth3.hooks.refreshSession(
opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, data
);
});
});
};
oauth3.resourceOwnerPassword = oauth3.requests.resourceOwnerPassword;
oauth3.requests.refreshToken = function (providerUri, opts) {
console.info('[oauth3.requests.refreshToken] called', providerUri, opts);
return oauth3.discover(providerUri, opts).then(function (directive) {
var prequest = core.urls.refreshToken(directive, opts);
return oauth3.request(prequest).then(function (req) {
var data = (req.originalData || req.data);
data.provider_uri = providerUri;
if (data.error) {
return oauth3.PromiseA.reject(oauth3.core.formatError(providerUri, data));
}
return oauth3.hooks.refreshSession(opts, data);
});
});
};
oauth3.refreshToken = oauth3.requests.refreshToken;
// TODO It'll be very interesting to see if we can do some browser popup stuff from the CLI
oauth3.requests._error_description = 'Not Implemented: Please override by including <script src="oauth3.browser.js"></script>';
oauth3.requests.authorizationRedirect = function (/*providerUri, opts*/) {
throw new Error(oauth3.requests._error_description);
};
oauth3.requests.implicitGrant = function (/*providerUri, opts*/) {
throw new Error(oauth3.requests._error_description);
};
oauth3.requests.logout = function (/*providerUri, opts*/) {
throw new Error(oauth3.requests._error_description);
};
oauth3.login = function (providerUri, opts) {
// Four styles of login:
// * background (hidden iframe)
// * iframe (visible iframe, needs border color and width x height params)
// * popup (needs width x height and positioning? params)
// * window (params?)
// Two strategies
// * authorization_redirect (to server authorization code)
// * implicit_grant (default, browser-only)
// If both are selected, implicit happens first and then the other happens in background
var promise;
if (opts.username || opts.password) {
/* jshint ignore:start */
// ingore "confusing use of !"
if (!opts.username !== !(opts.password || opts.otp)) {
throw new Error("you did not specify both username and password");
}
/* jshint ignore:end */
return oauth3.requests.resourceOwnerPassword(providerUri, opts).then(function (resp) {
if (!resp || !resp.data) {
var err = new Error("bad response");
err.response = resp;
err.data = resp && resp.data || undefined;
return oauth3.PromiseA.reject(err);
}
return resp.data;
});
}
// TODO support dual-strategy login
// by default, always get implicitGrant (for client)
// and optionally do authorizationCode (for server session)
if ('background' === opts.type || opts.background) {
opts.type = 'background';
opts.background = true;
}
else {
opts.type = 'popup';
opts.popup = true;
}
if (opts.authorizationRedirect) {
promise = oauth3.requests.authorizationRedirect(providerUri, opts);
}
else {
promise = oauth3.requests.implicitGrant(providerUri, opts);
}
return promise;
};
oauth3.backgroundLogin = function (providerUri, opts) {
opts = opts || {};
opts.type = 'background';
return oauth3.login(providerUri, opts);
};
oauth3.core = core;
oauth3.querystringify = core.querystringify;
oauth3.scopestringify = core.stringifyscope;
oauth3.stringifyscope = core.stringifyscope;
exports.OAUTH3 = oauth3.oauth3 = oauth3.OAUTH3 = oauth3;
exports.oauth3 = exports.OAUTH3;
if ('undefined' !== typeof module) {
module.exports = oauth3;
}
}('undefined' !== typeof exports ? exports : window));

158
prefactor/oauth3.lint.js Normal file
View File

@ -0,0 +1,158 @@
// TODO move to a test / lint suite?
oauth3._lintPromise = function (PromiseA) {
var promise;
var x = 1;
// tests that this promise has all of the necessary api
promise = new PromiseA(function (resolve, reject) {
//console.log('x [2]', x);
if (x !== 1) {
throw new Error("bad promise, create not Synchronous [0]");
}
PromiseA.resolve().then(function () {
var promise2;
//console.log('x resolve', x);
if (x !== 2) {
throw new Error("bad promise, resolve not Asynchronous [1]");
}
promise2 = PromiseA.reject().then(reject, function () {
//console.log('x reject', x);
if (x !== 4) {
throw new Error("bad promise, reject not Asynchronous [2]");
}
if ('undefined' === typeof angular) {
throw new Error("[NOT AN ERROR] Dear angular users: ignore this error-handling test");
} else {
return PromiseA.reject(new Error("[NOT AN ERROR] ignore this error-handling test"));
}
});
x = 4;
return promise2;
}).catch(function (e) {
if (e.message.match('NOT AN ERROR')) {
resolve({ success: true });
} else {
reject(e);
}
});
x = 3;
});
x = 2;
return promise;
};
oauth3._lintDirectives = function (providerUri, directives) {
var params = { directives: directives };
console.log('DEBUG oauth3._discoverHelper', directives);
var err;
if (!params.directives) {
err = new Error(params.error_description || "Unknown error when discoving provider '" + providerUri + "'");
err.code = params.error || "E_UNKNOWN_ERROR";
return OAUTH3.PromiseA.reject(err);
}
try {
directives = JSON.parse(atob(params.directives));
console.log('DEBUG oauth3._discoverHelper directives', directives);
} catch(e) {
err = new Error(params.error_description || "could not parse directives for provider '" + providerUri + "'");
err.code = params.error || "E_PARSE_DIRECTIVE";
return OAUTH3.PromiseA.reject(err);
}
if (
(directives.authorization_dialog && directives.authorization_dialog.url)
|| (directives.access_token && directives.access_token.url)
) {
// TODO lint directives
// TODO self-reference in directive for providerUri?
directives.provider_uri = providerUri;
localStorage.setItem('oauth3.' + providerUri + '.directives', JSON.stringify(directives));
localStorage.setItem('oauth3.' + providerUri + '.directives.updated_at', new Date().toISOString());
return OAUTH3.PromiseA.resolve(directives);
} else {
// ignore
console.error("the directives provided by '" + providerUri + "' were invalid.");
params.error = params.error || "E_INVALID_DIRECTIVE";
params.error_description = params.error_description
|| "directives did not include authorization_dialog.url";
err = new Error(params.error_description || "Unknown error when discoving provider '" + providerUri + "'");
err.code = params.error;
return OAUTH3.PromiseA.reject(err);
}
};
core.tokenState = function (session) {
var fresh;
fresh = (Date.now() / 1000) >= (parseInt(session._accessTokenData.exp) || 0);
if (!fresh) {
console.log("[os] isn't fresh", session._accessTokenData.exp);
}
};
oauth3._lintRequest = function (preq, opts) {
var providerUri;
console.log('[os] request meta opts', opts);
// check that the JWT is not expired
// TODO check that this request applies to the aud and azp
if (!(preq.session && preq.session.accessToken)) {
console.log('[os] no session/accessTokenData');
return oauth3.PromiseA.resolve(preq);
}
preq.headers = preq.headers || {};
preq.headers.Authorization = 'Bearer ' + preq.session.accessToken;
if (!preq.session._accessTokenData) {
console.log('[os] no _accessTokenData');
preq.session._accessTokenData = core.jwt.decode(preq.session.accessToken).payload;
}
if (!preq.url.match(preq.session._accessTokenData.aud)) {
console.log("[os] doesn't match audience", preq.session._accessTokenData.aud);
return oauth3.PromiseA.resolve(preq);
}
switch (core.tokenState(session)) {
case 'fresh':
return oauth3.PromiseA.resolve(preq);
case 'stale':
case 'useless':
break;
}
if (!preq.session.refreshToken) {
console.log("[os] can't refresh", preq.session);
return oauth3.PromiseA.resolve(preq);
}
opts.refreshToken = preq.session.refreshToken;
console.log('[oauth3.js] refreshToken attempt');
// TODO include directive?
providerUri = preq.session.providerUri || preq.session._accessTokenData.iss;
//opts.
return oauth3.refreshToken(providerUri, opts).then(function (res) {
console.log('[oauth3.js] refreshToken result:', res);
if (!res.data.accessToken) {
return preq;
}
// TODO fire session update event
res.data.providerUri = preq.session.providerUri;
preq.session = res.data;
preq.headers.Authorization = 'Bearer ' + preq.session.accessToken;
return preq;
});
};

View File

@ -0,0 +1,97 @@
;(function (exports) {
'use strict';
var OAUTH3 = window.OAUTH3 || require('./oauth3.js');
OAUTH3.authz = OAUTH3.authz || {};
OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
// OAuth3.requests.grants(providerUri, {}); // return list of grants
// OAuth3.checkGrants(providerUri, {}); //
var clientUri = OAUTH3.core.normalizeUri(clientParams.client_id || clientParams.client_uri);
var scope = clientParams.scope || '';
var clientObj = clientParams;
if (!scope) {
scope = 'oauth3_authn';
}
return OAUTH3.requests.grants(providerUri, {
method: 'GET'
, client_id: clientUri
, client_uri: clientUri
, session: session
}).then(function (grants) {
var myGrants;
var grantedScopes;
var grantedScopesMap;
var pendingScopes;
var acceptedScopes;
var acceptedScopesMap;
var scopes = OAUTH3.core.parsescope(scope);
var callbackUrl;
console.log('previous grants:');
console.log(grants);
// it doesn't matter who the referrer is as long as the destination
// is an authorized destination for the client in question
// (though it may not hurt to pass the referrer's info on to the client)
if (!OAUTH3.checkRedirect(grants.client, clientObj)) {
callbackUrl = 'https://oauth3.org/docs/errors#E_REDIRECT_ATTACK'
+ '?redirect_uri=' + clientObj.redirect_uri
+ '&allowed_urls=' + grants.client.url
+ '&client_id=' + clientUri
+ '&referrer_uri=' + OAUTH3.core.normalizeUri(window.document.referrer)
;
location.href = callbackUrl;
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) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
return true;
}
});
grantedScopesMap = {};
acceptedScopesMap = {};
pendingScopes = scopes.filter(function (requestedScope) {
return myGrants.every(function (grant) {
if (!grant.scope) {
grant.scope = 'oauth3_authn';
}
var gscopes = grant.scope.split(/[+, ]/g);
gscopes.forEach(function (s) { grantedScopesMap[s] = true; });
if (-1 !== gscopes.indexOf(requestedScope)) {
// already accepted in the past
acceptedScopesMap[requestedScope] = true;
}
else {
// true, is pending
return true;
}
});
});
grantedScopes = Object.keys(grantedScopesMap);
acceptedScopes = Object.keys(acceptedScopesMap);
return {
pending: pendingScopes // not yet accepted
, granted: grantedScopes // all granted, ever
, requested: scopes // all requested, now
, accepted: acceptedScopes // granted (ever) and requested (now)
, client: grants.client
, grants: grants.grants
};
});
};
exports.OAUTH3_PROVIDER = OAUTH3;
if ('undefined' !== typeof module) {
module.exports = OAUTH3;
}
}('undefined' !== typeof exports ? exports : window));

View File

@ -0,0 +1,24 @@
var separator;
// TODO check that we appropriately use '#' for implicit and '?' for code
// (server-side) in an OAuth2 backwards-compatible way
if ('token' === scope.appQuery.response_type) {
separator = '#';
}
else if ('code' === scope.appQuery.response_type) {
separator = '?';
}
else {
separator = '#';
}
if (scope.pendingScope.length && !opts.allow) {
redirectUri += separator + Oauth3.querystringify({
error: 'access_denied'
, error_description: 'None of the permissions were accepted'
, error_uri: 'https://oauth3.org/docs/errors#access_denied'
, state: scope.appQuery.state
});
$window.location.href = redirectUri;
return;
}

View File

@ -1 +0,0 @@
_apis

View File

@ -0,0 +1,13 @@
{ "terms": [ "oauth3.org/tos/draft" ]
, "api": "api.:hostname"
, "authorization_dialog": { "url": "#/authorization_dialog" }
, "access_token": { "method": "POST", "url": "api/issuer@oauth3.org/access_token" }
, "otp": { "method": "POST", "url": "api/issuer@oauth3.org/otp" }
, "credential_otp": { "method": "POST", "url": "api/issuer@oauth3.org/otp" }
, "credential_meta": { "url": "api/issuer@oauth3.org/logins/meta/:type/:id" }
, "credential_create": { "method": "POST", "url": "api/issuer@oauth3.org/logins" }
, "grants": { "method": "GET", "url": "api/issuer@oauth3.org/grants/:azp/:sub" }
, "authorization_decision": { "method": "POST", "url": "api/issuer@oauth3.org/authorization_decision" }
, "callback": { "method": "GET", "url": ".well-known/oauth3/callback.html#/" }
, "logout": { "method": "GET", "url": "#/logout/" }
}

View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
background-color: #ffcccc;
}
</style>
</head>
<body>
OAuth3 RPC
<script src="../../assets/oauth3.org/oauth3.core.js"></script>
<script>
;(function () {
'use strict';
// Taken from oauth3.core.js
// TODO what about search within hash?
var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/]";
var params = OAUTH3.query.parse(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);
OAUTH3.request({ url: 'directives.json' }).then(function (resp) {
var urlsafe64 = OAUTH3._base64.encodeUrlSafe(JSON.stringify(resp.data, null, 0));
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.query.stringify({
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>