Compare commits

..

No commits in common. "master" and "v1.0.1" have entirely different histories.

50 changed files with 3299 additions and 19238 deletions

4
.gitignore vendored
View File

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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "node_modules/terminal-forms.js"]
path = node_modules/terminal-forms.js
url = git@git.daplie.com:/OAuth3/terminal-forms.js

View File

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

View File

@ -1,16 +0,0 @@
{ "node": true
, "browser": true
, "jquery": true
, "strict": true
, "indent": 2
, "onevar": true
, "laxcomma": true
, "laxbreak": true
, "eqeqeq": true
, "immed": true
, "undef": true
, "unused": true
, "latedef": true
, "curly": true
, "trailing": true
}

View File

@ -1,5 +0,0 @@
.git*
browserify/
prefactor/
gulpfile.js
bump-version.sh

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.

152
README.md
View File

@ -1,12 +1,6 @@
oauth3.js 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 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!) (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/` 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/` 2. Inside of the folder `example.com/` a folder called `assets/`
3. Inside of the folder `example.com/assets` a folder called `oauth3.org/` 3. Inside of the folder `example.com/assets` a folder called `org.oauth3/`
4. Download [oauth3.js-v1.zip](https://git.oauth3.org/OAuth3/oauth3.js/repository/archive.zip?ref=v1) 4. Download [oauth.js-v1.zip](https://git.daplie.com/Daplie/oauth3.js/repository/archive.zip?ref=v1)
5. Double-click to unzip the folder. 5. Double-click to unzip the folder.
6. Copy the file `oauth3.core.js` into the folder `example.com/assets/oauth3.org/` 6. Copy the file `oauth3.core.js` into the folder `example.com/assets/org.oauth3/`
7. Copy the folder `_apis` into the folder `example.com/` 7. Copy the folder `well-known` into the folder `example.com/`
9. Add `<script src="assets/oauth3.org/oauth3.core.js"></script>` to your `index.html` 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` 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: 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" <script src="https://code.jquery.com/jquery-3.1.1.js"
integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA=" integrity="sha256-16cdPddA6VdVInumRGo6IbivbERE8p7CQR3HzTBuELA="
crossorigin="anonymous"></script> 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> <script src="app.js"></script>
</body> </body>
</html> </html>
@ -58,15 +53,15 @@ If you have no idea what you're doing
`app.js`: `app.js`:
```js ```js
var OAUTH3 = window.OAUTH3; 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 // 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 // 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 // example https://oauth3.org
return oauth3.setIdentityProvider(providerUri); return auth.setProvider(providerUri);
} }
@ -74,7 +69,7 @@ function onChangeProvider(providerUri) {
// //
function onClickLogin() { function onClickLogin() {
return oauth3.authenticate().then(function (session) { return auth.authenticate().then(function (session) {
console.info('Authentication was Successful:'); console.info('Authentication was Successful:');
console.log(session); console.log(session);
@ -85,14 +80,12 @@ function onClickLogin() {
// //
console.info('Secure PPID (aka subject):', session.token.sub); console.info('Secure PPID (aka subject):', session.token.sub);
return oauth3.request({ return auth.request({
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid' url: 'https://oauth3.org/api/org.oauth3.provider/inspect'
.replace(/:sub/g, session.token.sub)
.replace(/:kid/g, session.token.kid || session.token.iss)
, session: session , session: session
}).then(function (resp) { }).then(function (resp) {
console.info("Signing Public Key JWK:"); console.info("Inspect Token:");
console.log(resp.data); console.log(resp.data);
}); });
@ -109,7 +102,7 @@ function onClickLogin() {
// //
function onClickLogout() { function onClickLogout() {
return oauth3.logout().then(function () { return auth.logout().then(function () {
localStorage.clear(); localStorage.clear();
console.info('Logout was Successful'); console.info('Logout was Successful');
@ -145,13 +138,13 @@ it might look like this:
example.com example.com
├── _apis ├── .well-known (hidden)
│   └── oauth3.org │   └── oauth3
│   ├── callback.html │   ├── callback.html
│   ├── directives.json │   ├── directives.json
│   └── index.html │   └── index.html
├── assets ├── assets
│   └── oauth3.org │   └── org.oauth3
│   └── oauth3.core.js │   └── oauth3.core.js
@ -172,17 +165,17 @@ Installation (if you know what you're doing)
pushd /path/to/your/web/app pushd /path/to/your/web/app
# clone the project as assets/oauth3.org # clone the project as assets/org.oauth3
mkdir -p assets mkdir -p assets
git clone git@git.oauth3.org:OAuth3/oauth3.js.git assets/oauth3.org git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
pushd assets/oauth3.org pushd assets/org.oauth3
git checkout v1 git checkout v1
popd popd
# symlink `_apis/oauth3.org` to `assets/oauth3.org/_apis/oauth3.org` # symlink `.well-known/oauth3` to `assets/org.oauth3/.well-known/oauth3`
mkdir -p _apis mkdir -p .well-known
ln -sf ../assets/oauth3.org/_apis/oauth3 _apis/oauth3.org ln -sf ../assets/org.oauth3/.well-known/oauth3 .well-known/oauth3
``` ```
**Advanced Installation with `bower`** **Advanced Installation with `bower`**
@ -192,17 +185,17 @@ ln -sf ../assets/oauth3.org/_apis/oauth3 _apis/oauth3.org
bower install oauth3 bower install oauth3
# create a `_apis` folder and an `assets` folder # create a `.well-known` folder and an `assets` folder
mkdir -p _apis assets mkdir -p .well-known assets
# symlink `_apis/oauth3.org` to `bower_components/oauth3.org/_apis/oauth3.org` # symlink `.well-known/oauth3` to `bower_components/oauth3/.well-known/oauth3`
ln -sf ../bower_components/oauth3.org/_apis/oauth3.org _apis/oauth3.org ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
# symlink `assets/oauth3.org` to `bower_components/oauth3.org` # symlink `assets/org.oauth3` to `bower_components/oauth3`
ln -sf ../bower_components/oauth3.org/_apis/oauth3.org _apis/oauth3.org ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
ln -sf ../bower_components/oauth3.org assets/oauth3.org ln -sf ../bower_components/oauth3 assets/org.oauth3
``` ```
Usage Usage
@ -211,14 +204,13 @@ Usage
Update your HTML to include the the following script tag: Update your HTML to include the the following script tag:
```html ```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: You can create a very simple demo application like this:
```javascript ```javascript
var providerUri; var providerUri;
var opts = { client_uri: OAUTH3.utils.clientUri(window.location) };
// this is any OAuth3-compatible provider, such as oauth3.org // this is any OAuth3-compatible provider, such as oauth3.org
@ -226,7 +218,7 @@ var opts = { client_uri: OAUTH3.utils.clientUri(window.location) };
// //
function onChangeProvider(_providerUri) { function onChangeProvider(_providerUri) {
providerUri = _providerUri; providerUri = _providerUri;
return OAUTH3.discover(providerUri, opts); // just to cache return OAUTH3.discover(providerUri); // just to cache
} }
@ -234,6 +226,7 @@ function onChangeProvider(_providerUri) {
// //
function onClickLogin() { function onClickLogin() {
var opts = { client_uri: OAuth3.clientUri(window.location) };
return OAUTH3.implicitGrant(providerUri, opts).then(function (session) { return OAUTH3.implicitGrant(providerUri, opts).then(function (session) {
console.info('Authentication was Successful:'); console.info('Authentication was Successful:');
@ -246,7 +239,7 @@ function onClickLogin() {
console.info('Secure PPID (aka subject):', session.token.sub); console.info('Secure PPID (aka subject):', session.token.sub);
return OAUTH3.request({ return OAUTH3.request({
url: 'https://oauth3.org/api/issuer@oauth3.org/inspect_token' url: 'https://oauth3.org/api/org.oauth3.provider/inspect_token'
, session: session , session: session
}).then(function (resp) { }).then(function (resp) {
@ -267,18 +260,6 @@ function onClickLogin() {
onChangeProvider('oauth3.org'); onChangeProvider('oauth3.org');
``` ```
A user's e-mail can be passed into the clientParams object as `clientParams.subject`.
To auto-populate the e-mail input of the login popup, make sure the input has the class `js-oauth3-email`.
Example:
```js
if (clientParams.subject) {
$('.js-oauth3-email').val(clientParams.subject);
$('.js-authn-show').prop('disabled', false);
}
```
### Compatibility with Frameworks and Libraries ### Compatibility with Frameworks and Libraries
**jQuery**: **jQuery**:
@ -290,19 +271,7 @@ You're all set. Nothing else is needed.
We've created an `Oauth3` service just for you: We've created an `Oauth3` service just for you:
```html ```html
<script src="assets/oauth3.org/oauth3.ng.js"></script> <script src="assets/org.oauth3/oauth3.ng.js"></script>
```
```js
// Require the module as 'oauth3.org'
var app = angular.module('myAppName', [ 'ui.router', 'oauth3.org' ]);
// Require services and other submodules in the form {modulename}@oauth3.org
app.controller('authCtrl', [ '$scope', 'azp@oauth3.org', function ($scope, Oauth3) { /* ... */ } ]);
// For backwards compatibility with older angular applications that rely on string-name introspection
// you can also use the camel case version of the names in the format {Modulename}Oauth3
app.controller('authCtrl', function ($scope, AzpOauth3) { /* ... */ });
``` ```
You can include that in addition to the standard file or, You can include that in addition to the standard file or,
@ -315,48 +284,31 @@ We include a small wrapper function of just a few lines in the bottom of `oauth3
which exposes a `create` method to make using the underlying library require typing fewer keystrokes. which exposes a `create` method to make using the underlying library require typing fewer keystrokes.
``` ```
oauth3 = OAUTH3.create(location); // takes a location object, such as window.location auth = OAUTH3.create(location); // takes a location object, such as window.location
// to create the Client URI (your app's id) // to create the Client URI (your app's id)
// and save it to an internal state // and save it to an internal state
promise = oauth3.init(opts); // set and fetch your own site/app's configuration details promise = auth.init(location); // set and fetch your own site/app's configuration details
// promises your site's config // opts = { location, session, issuer, audience } // promises your site's config
promise = oauth3.setIdentityProvider(url); // changes the Identity Provider URI (the site you're logging into), promise = auth.setProvider(url); // changes the 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 // 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) promise = auth.authenticate(); // opens login window for the provider and returns a session
// (must be called after the setProvider promise has completed)
promise = oauth3.setProvider(url); // changes the both Identity and Resource Provider URI together promise = auth.authorize(permissions); // authenticates (if not authenticated) and opens a window to
promise = oauth3.authenticate(); // opens login window for the provider and returns a session
// (must be called after the setIdentityProvider promise has completed)
promise = oauth3.authorize(permissions); // authenticates (if not authenticated) and opens a window to
// authorize a particular scope (contacts, photos, whatever) // authorize a particular scope (contacts, photos, whatever)
promise = oauth3.request({ url, method, data }); // make an (authorized) arbitrary request to an audience's resource promise = auth.request({ url, method, data }); // make an (authorized) request to a provider's resource
// (contacts, photos, whatever) // (contacts, photos, whatever)
promise = oauth3.api(apiname, opts); // make an (authorized) well-known api call to an audience promise = auth.logout(); // opens logout window for the provider
// Ex: oauth3.api('dns.list', { sld: 'example', tld: 'com' });
// TODO auth.session(); // returns the current session, if any
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');
// api.list({ sld: 'mydomain', tld: 'com' });
promise = oauth3.logout(); // opens logout window for the provider
oauth3.session(); // returns the current session, if any
``` ```
<!-- TODO
Track down the old https://labs.daplie.com/docs/ for API schemas
--
Real API Real API
---------- ----------
@ -484,10 +436,10 @@ Since we do not require the `protocol` to be specified, it is a URI
However, we do have a problem of disambiguation since a URI may look like a `path`: However, we do have a problem of disambiguation since a URI may look like a `path`:
1. https://example.com/api/issuer@oauth3.org 1. https://example.com/api/org.oauth3.provider
2. example.com/api/issuer@oauth3.org/ (not unique) 2. example.com/api/org.oauth.provider/ (not unique)
3. /api/issuer@oauth3.org 3. /api/org.oauth3.provider
4. api/issuer@oauth3.org (not unique) 4. api/org.oauth3.provider (not unique)
Therefore anywhere a URI or a Path could be used, the URI must be a URL. Therefore anywhere a URI or a Path could be used, the URI must be a URL.
We eliminate #2. We eliminate #2.
@ -498,5 +450,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) 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 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. 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

@ -1,217 +0,0 @@
'use strict';
var oauth3 = require('./oauth3.js');
var defaults = {
main: 'oauth3'
, provider: 'oauth3.org'
};
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
var command = args.shift() || 'help';
var cmdpair = command.split(sep);
var cmd = cmdpair[0];
var sub = cmdpair[1];
var COMMAND = 'COMMAND';
var maxCmdLen = COMMAND.length;
var maxPairLen = 0;
var arg1 = args[0];
// build top-level commands (tlcs) list
// also count the word-width (for the space needed to print the commands)
var pairsMap = {};
var tlcs = opts.commands.filter(function (desc) {
var pair = desc[0].split(/\s+/)[0];
var psub = pair.split(sep)[0];
pairsMap[pair] = true;
maxPairLen = Math.max(maxPairLen, pair.length);
if (pair === psub) {
maxCmdLen = Math.max(maxCmdLen, psub.length);
return true;
}
});
// right pad (for making the printed lines longer)
function rpad(str, len) {
while (str.length < len) {
str += ' ';
}
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) {
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);
}
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('');
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'");
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, {
// CLI goals:
//
// whoami / login: you are now logged in as
// * john@example.com [current] (just now)
// * john@work.net (2 minutes ago)
// * john@family.me (2 weeks ago)
commands: [
[ 'login [email or cloud address]', 'alias of session:attach', [
"--auto, create a new account without asking if none exists"
//, "--exclusive, logout all other ids, removing access to their accounts"
, "--provider, specify an authentication provider (default: :provider)".replace(/\b:provider\b/, defaults.provider)
//, "--email [addr], use the given id as an email address, even if it works as a cloud address"
//, "--cloud [addr], use the given id as a cloud address or fail (don't fallback to email)"
]
]
, [ 'logout', 'alias of session:detach' ]
, [ 'whoami', 'show current account(s) and login(s) and device(s)' ]
// authn
, [ 'session', 'Manage your ids (credentials / logins)' ]
, [ 'session:new', 'alias of `login --exclusive`' ]
, [ 'session:attach', 'Create a session (and account if needed) for a given email address or cloud address' ]
, [ 'session:detach', 'remove login from session' ]
, [ 'session:list', 'show all of the ids in the current session' ]
// authz
, [ 'accounts', 'Manage your accounts (authorization / profiles)' ]
, [ 'accounts:new', 'create a new account attached to the credentials of the current session' ]
, [ 'accounts:set', 'change account details' ] // todo changing the name should be restricted john@provider.net -> jonathan@provider.net would be bad
, [ 'accounts:list', 'show all of the accounts in the current session' ]
, [ 'accounts:attach', 'attach an account to an id' ]
, [ 'accounts:detach', 'detach an account from an id' ]
, [ 'accounts:select', 'select an account to use as the primary account for this session' ]
, [ 'accounts:update', '(deprecated) alias of set' ]
, [ 'accounts:login', '(deprecated) alias of login' ]
, [ 'accounts:whoami', '(deprecated) alias of whoami' ]
// 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: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)' ]
, [ 'devices:list', 'show all devices for your account(s)' ]
// help
, [ 'help', "show this menu; use '" + defaults.main + " help COMMAND' (even 'help') for options and sub-commands" ]
]
});

View File

@ -15,33 +15,52 @@ OAUTH3._hooks.session.get = require('../oauth3.node.storage.js').sessions.get;
OAUTH3._hooks.session.set = require('../oauth3.node.storage.js').sessions.set; OAUTH3._hooks.session.set = require('../oauth3.node.storage.js').sessions.set;
*/ */
// opts = { email, providerUri }
module.exports.login = function (options) {
options = options || {};
var url = require('url'); var url = require('url');
//console.log('stdin tty', process.stdin.isTTY); //console.log('stdin tty', process.stdin.isTTY);
//console.log('stdout tty', process.stdout.isTTY); //console.log('stdout tty', process.stdout.isTTY);
var oauth3; var oauth3;
var opts = { var opts = {
email: options.email providerUri: undefined
, providerUri: options.providerUri
}; };
if (opts.form) {
form = opts.form; function getCurrentUserEmail() {
} return form.ask({ label: "What's your email (or cloud mail) address? ", type: 'email' }).then(function (emailResult) {
var email; var emailParts = (emailResult.result || emailResult.input).split('@');
var providerUrl; var domain = emailParts[1];
var providerUri; var username;
var sameProvider; var sameProvider;
var username;
function getSession() { var urlObj = url.parse(opts.providerUri || domain);
var username; // TODO get unique client id for bootstrapping app
oauth3 = OAUTH3.create(urlObj);
form.println("got to setProvider");
return oauth3.setProvider(domain).then(function () {
form.println("got to setProvider SUCCESS");
sameProvider = true;
// ignore
}, function () {
form.println("got to setProvider ERROR");
function askOauth3Url() {
return form.ask({ label: "What's your OAuth3 Provider URL? ", type: 'url' }).then(function (urlResult) {
var urlObj = url.parse(urlResult.result || urlResult.input);
// TODO get unique client id for bootstrapping app
oauth3 = OAUTH3.create(urlObj);
return oauth3.setProvider(urlResult.result || urlResult.input).then(function () {
// ignore
}, function (err) {
form.println(err.stack || err.message || err.toString());
return askOauth3Url();
});
});
}
return askOauth3Url();
}).then(function () {
// TODO lookup uuid locally before performing loginMeta // TODO lookup uuid locally before performing loginMeta
// TODO lookup token locally before performing loginMeta / otp // TODO lookup token locally before performing loginMeta / otp
return OAUTH3.authn.loginMeta(oauth3._providerDirectives, { email: email }).then(function (/*result*/) { form.println("got to loginMeta");
return { node: email, type: 'email' }; return OAUTH3.authn.loginMeta(oauth3._providerDirectives, { email: emailResult.input }).then(function (/*result*/) {
return { node: emailResult.result || emailResult.input, type: 'email' };
}, function (/*err*/) { }, function (/*err*/) {
// TODO require hashcash to create user account // TODO require hashcash to create user account
function confirmCreateAccount() { function confirmCreateAccount() {
@ -62,14 +81,15 @@ module.exports.login = function (options) {
} }
if (!sameProvider) { if (!sameProvider) {
return { node: email, type: 'email' }; return { node: emailResult.result || emailResult.input, type: 'email' };
} }
return form.ask({ return form.ask({
label: "What's your recovery email (or cloud mail) address? ", type: 'email' label: "What's your recovery email (or cloud mail) address? ", type: 'email'
}).then(function (recoveryResult) { }).then(function (recoveryResult) {
username = emailParts[0];
return { return {
node: email node: emailResult.result || emailResult.input
, type: 'name' , type: 'name'
, recovery: recoveryResult.result || recoveryResult.input , recovery: recoveryResult.result || recoveryResult.input
}; };
@ -78,7 +98,12 @@ module.exports.login = function (options) {
} }
return confirmCreateAccount(); return confirmCreateAccount();
}).then(function (user) { });
});
});
}
return getCurrentUserEmail().then(function (user) {
// TODO skip if token exists locally // TODO skip if token exists locally
var email = (user.recovery || user.node); var email = (user.recovery || user.node);
form.println("Sending login code to '" + email + "'..."); form.println("Sending login code to '" + email + "'...");
@ -115,91 +140,13 @@ module.exports.login = function (options) {
// returns session instead of input // returns session instead of input
var colors = require('colors'); var colors = require('colors');
form.setStatus(colors.dim("authenticating with server...")); form.setStatus(colors.dim("authenticating with server..."));
return OAUTH3.authn.resourceOwnerPassword(oauth3._providerDirectives, data).then(function (result) { return OAUTH3.authn.resourceOwnerPassword(oauth3._providerDirectives, data);
return result;
}, function (/*err*/) {
// TODO test error
return form.PromiseA.reject(new Error("The code '" + formatted + "' is mistyped or incorrect. Double check and try again."));
});
}
});
});
});
} }
}).then(function (results) {
var session = results.result;
function getCurrentUserEmail() { form.println('session:');
return form.ask({ form.println(session);
label: "What's your email (or cloud mail) address? ", type: 'email', value: opts.email
}).then(function (emailResult) {
opts.email = undefined;
email = (emailResult.result || emailResult.input);
var emailParts = email.split('@');
var domain = emailParts[1];
username = emailParts[0];
providerUrl = 'https://' + domain;
providerUri = domain;
var urlObj = url.parse(providerUrl);
// TODO get unique client id for bootstrapping app
oauth3 = OAUTH3.create(urlObj);
return oauth3.setProvider(domain).then(function () {
sameProvider = true;
// ignore
}, function () {
function askOauth3Url() {
return form.ask({
label: "What's your OAuth3 Provider URL? ", type: 'url', value: opts.providerUri
}).then(function (urlResult) {
opts.providerUri = undefined;
providerUrl = urlResult.result || urlResult.input;
providerUri = OAUTH3.uri.normalize(providerUrl);
var urlObj = url.parse(providerUrl);
// TODO get unique client id for bootstrapping app
oauth3 = OAUTH3.create(urlObj);
return oauth3.setProvider(providerUri).then(function () {
// ignore
}, function (err) {
form.println(err.stack || err.message || err.toString());
return askOauth3Url();
});
});
}
return askOauth3Url();
});
});
}
return getCurrentUserEmail().then(function () {
return OAUTH3._hooks.meta.get(email).then(function (id) {
if (!id) {
return null;
}
return OAUTH3._hooks.sessions.get(providerUri, id).then(function (session) {
if (session) {
return session;
}
return null;
});
});
}).then(function (session) {
if (session) {
return session;
}
return getSession().then(function (sessionResult) {
var session = sessionResult.result;
var id = require('crypto').createHash('sha256').update(session.token.sub || '').digest('hex');
return OAUTH3._hooks.sessions.set(providerUri, session, id).then(function (session) {
return OAUTH3._hooks.meta.set(email, id).then(function () {
return session;
}); });
}); });
}); });
}).then(function (session) {
oauth3.__session = session;
return oauth3;
});
};

View File

@ -26,18 +26,13 @@
"sign" "sign"
], ],
"license": "MIT", "license": "MIT",
"homepage": "https://git.oauth3.org/OAuth3/oauth3.js", "homepage": "https://git.daplie.com/OAuth3/oauth3.js",
"ignore": [ "ignore": [
"**/.*", "**/.*",
"browserify",
"prefactor",
"gulpfile.js",
"bump-version.sh",
"oauth3.node.*",
"node_modules", "node_modules",
"bower_components", "bower_components",
"test", "test",
"tests" "tests"
], ],
"version": "1.0.10" "version": "1.0.1"
} }

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);
});
});
});
}
};
}());

1
node_modules/terminal-forms.js generated vendored Submodule

@ -0,0 +1 @@
Subproject commit f078d479b085e8658fb2039eb3e4de49afd9db0e

View File

@ -1,86 +0,0 @@
;(function (exports) {
'use strict';
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
OAUTH3.api['account.listCards'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.payments/accounts/' + session.token.sub + '/cards'
, session: session
}).then(function (res) {
return res;
});
};
OAUTH3.api['account.addCard'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.payments/accounts/' + session.token.sub + '/cards'
, session: session
, data: opts.data
}).then(function (res) {
return res;
});
};
OAUTH3.api['account.removeCard'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'DELETE'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.payments/accounts/' + session.token.sub + '/cards/' + opts.last4 + '/' + opts.brand
, session: session
}).then(function (res) {
return res;
});
};
OAUTH3.api['account.listAddresses'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.me/accounts/' + session.token.sub + '/addresses'
, session: session
}).then(function (res) {
return res;
});
};
OAUTH3.api['account.addAddress'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.me/accounts/' + session.token.sub + '/addresses'
, session: session
, data: opts.addAddress
}).then(function (res) {
return res;
});
};
OAUTH3.api['account.removeAddress'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'DELETE'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.me/accounts/' + session.token.sub + '/addresses/' + opts.addressId
, session: session
}).then(function (res) {
return res;
});
};
}('undefined' !== typeof exports ? exports : window));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -8,9 +8,6 @@
OAUTH3.crypto.core = require('./oauth3.node.crypto'); OAUTH3.crypto.core = require('./oauth3.node.crypto');
} catch (error) { } catch (error) {
OAUTH3.crypto.core = {}; 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 // 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. // it directly to the core object instead of the webCrypto object.
@ -20,31 +17,10 @@
}; };
var webCrypto = {}; 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) { webCrypto.sha256 = function (buf) {
return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf); return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
}; };
OAUTH3.crypto.core.pbkdf2 = deferCryptoCall("pbkdf2");
webCrypto.pbkdf2 = function (password, salt) { webCrypto.pbkdf2 = function (password, salt) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey']) return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey'])
.then(function (key) { .then(function (key) {
@ -56,15 +32,12 @@
}); });
}; };
OAUTH3.crypto.core.encrypt = deferCryptoCall("encrypt");
webCrypto.encrypt = function (rawKey, iv, data) { webCrypto.encrypt = function (rawKey, iv, data) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt']) return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt'])
.then(function (key) { .then(function (key) {
return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data); 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) { webCrypto.decrypt = function (rawKey, iv, data) {
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt']) return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt'])
.then(function (key) { .then(function (key) {
@ -72,7 +45,6 @@
}); });
}; };
OAUTH3.crypto.core.genEcdsaKeyPair = deferCryptoCall("genEcdsaKeyPair");
webCrypto.genEcdsaKeyPair = function () { webCrypto.genEcdsaKeyPair = function () {
return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
.then(function (keyPair) { .then(function (keyPair) {
@ -85,7 +57,6 @@
}); });
}; };
OAUTH3.crypto.core.sign = deferCryptoCall("sign");
webCrypto.sign = function (jwk, msg) { webCrypto.sign = function (jwk, msg) {
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign'])
.then(function (key) { .then(function (key) {
@ -95,8 +66,6 @@
return new Uint8Array(sig); return new Uint8Array(sig);
}); });
}; };
OAUTH3.crypto.core.verify = deferCryptoCall("verify");
webCrypto.verify = function (jwk, msg, signature) { webCrypto.verify = function (jwk, msg, signature) {
// If the JWK has properties that should only exist on the private key or is missing // 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. // "verify" in the key_ops, importing in as a public key won't work.
@ -113,7 +82,6 @@
}; };
function checkWebCrypto() { function checkWebCrypto() {
/* global OAUTH3_crypto_fallback */
var loadFallback = function() { var loadFallback = function() {
var prom; var prom;
loadFallback = function () { return prom; }; loadFallback = function () { return prom; };
@ -128,25 +96,25 @@
resolve(); resolve();
} }
}; };
script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js'; script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js';
body.appendChild(script); body.appendChild(script);
}); });
return prom; return prom;
}; };
function checkException(name, func) { function checkException(name, func) {
return OAUTH3.PromiseA.resolve().then(func) new OAUTH3.PromiseA(function (resolve) { resolve(func()); })
.then(function () { .then(function () {
OAUTH3.crypto.core[name] = webCrypto[name]; OAUTH3.crypto.core[name] = webCrypto[name];
}, function (err) { })
.catch(function (err) {
console.warn('error with WebCrypto', name, '- using fallback', 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]; OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
}); });
}); });
} }
function checkResult(name, expected, func) { function checkResult(name, expected, func) {
checkException(name, function () {
finishBeforeReady.push(checkException(name, function () {
return func() return func()
.then(function (result) { .then(function (result) {
if (typeof expected === typeof result) { if (typeof expected === typeof result) {
@ -159,7 +127,7 @@
throw new Error("result ("+result+") doesn't match expectation ("+expected+")"); 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]); 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); return webCrypto.verify(jwk, dataBuf, sig);
}); });
// The results of these functions are less predictable, so we can't check their return value. // 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(); 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(); checkWebCrypto();
@ -228,67 +188,105 @@
return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing)); return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing));
} }
// I'm not actually 100% sure this behavior is guaranteed, but when we use an array as the var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}';
// replacer argument the keys are always in the order they appeared in the array.
var jwkStr = JSON.stringify(jwk, keys);
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr)) return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr))
.then(OAUTH3._base64.bufferToUrlSafe); .then(OAUTH3._base64.bufferToUrlSafe);
}; };
OAUTH3.crypto.createKeyPair = function () { OAUTH3.crypto._createKey = function (ppid) {
// TODO: maybe support other types of key pairs, not just ECDSA P-256 var saltProm = OAUTH3.crypto.core.randomBytes(16);
return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) { 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) { return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256'; keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
keyPair.privateKey.kid = keyPair.publicKey.kid = kid; keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
return keyPair; 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([ return OAUTH3.PromiseA.all([
kekProm kekProm
, ecdsaProm
, saltProm , saltProm
, OAUTH3.crypto.core.randomBytes(16)
, OAUTH3.crypto.core.randomBytes(12) , OAUTH3.crypto.core.randomBytes(12)
, ]).then(function (results) { , OAUTH3.crypto.core.randomBytes(12)
]).then(function (results) {
var kek = results[0]; var kek = results[0];
var salt = results[1]; var keyPair = results[1];
var ecdsaIv = results[2]; 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.PromiseA.all([
return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) { OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)))
, OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret)
])
.then(function (encrypted) {
return { return {
publicKey: keyPair.publicKey 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) , salt: OAUTH3._base64.bufferToUrlSafe(salt)
, ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv) , 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 salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt);
var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);
return OAUTH3.crypto.core.pbkdf2(password, salt) return OAUTH3.crypto.core.pbkdf2(ppid, salt)
.then(function (key) { .then(function (key) {
return OAUTH3.crypto.core.decrypt(key, iv, encJwk); return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
}) })
.then(OAUTH3._binStr.bufferToBinStr) .then(OAUTH3._binStr.bufferToBinStr)
.then(JSON.parse) .then(JSON.parse);
.then(function (privateKey) { };
return {
privateKey: privateKey OAUTH3.crypto._getKey = function (ppid) {
, publicKey: storedObj.publicKey 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

@ -31,23 +31,15 @@ OAUTH3.api['devices.list'] = function (providerUri, opts) {
OAUTH3.api['devices.attach'] = function (providerUri, opts) { OAUTH3.api['devices.attach'] = function (providerUri, opts) {
var session = opts.session; var session = opts.session;
var device = opts.device;
var tld = opts.tld;
var sld = opts.sld;
var sub = opts.sub;
var ip = opts.ip;
var ttl = opts.ttl;
return OAUTH3.request({ return OAUTH3.request({
url: OAUTH3.url.normalize(providerUri) url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/devices/' + '/api/com.daplie.domains/accounts/' + session.token.sub
+ device + '/' + tld + '/' + sld + '/' + (sub || '') //+ '/devices/' + device + '/'
+ '/devices/' + (opts.data.uid || '_') + '/' + opts.data.device
+ '/' + opts.data.tld + '/' + opts.data.sld + '/' + (opts.data.sub || '')
, method: 'POST' , method: 'POST'
, session: session , session: session
, data: {
addresses: ip
, ttl: ttl
}
}, {}).then(function (res) { }, {}).then(function (res) {
return res.data.devices || res.data; return res.data.devices || res.data;
}); });
@ -55,15 +47,28 @@ OAUTH3.api['devices.attach'] = function (providerUri, opts) {
OAUTH3.api['devices.detach'] = function (providerUri, opts) { OAUTH3.api['devices.detach'] = function (providerUri, opts) {
var session = opts.session; var session = opts.session;
var device = opts.device;
var tld = opts.tld;
var sld = opts.sld;
var sub = opts.sub;
return OAUTH3.request({ return OAUTH3.request({
url: OAUTH3.url.normalize(providerUri) url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/api/com.daplie.domains/accounts/' + session.token.sub
+ '/devices/' + device + '/' + tld + '/' + sld + '/' + (sub || '') //+ '/devices/' + device + '/'
+ '/devices/' + (opts.data.uid || '_') + '/' + opts.data.device
+ '/' + opts.data.tld + '/' + opts.data.sld + '/' + (opts.data.sub || '')
, method: 'DELETE'
, session: session
}, {}).then(function (res) {
return res.data.devices || res.data;
});
};
OAUTH3.api['devices.detach'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub
+ '/devices/' + opts.data.device
+ '/' + opts.data.tld + '/' + opts.data.sld + '/' + (opts.data.sub || '')
, method: 'DELETE' , method: 'DELETE'
, session: session , session: session
}, {}).then(function (res) { }, {}).then(function (res) {
@ -71,69 +76,4 @@ OAUTH3.api['devices.detach'] = function (providerUri, opts) {
}); });
}; };
OAUTH3.api['devices.destroy'] = function (providerUri, opts) {
var session = opts.session;
var device = opts.device;
return OAUTH3.request({
url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub
+ '/devices/' + device
, method: 'DELETE'
, session: session
}, {}).then(function (res) {
return res.data.device || res.data;
});
};
OAUTH3.api['dns.set'] = function (providerUri, opts) {
var session = opts.session;
var tld = opts.tld;
var sld = opts.sld;
var sub = opts.sub;
var type = opts.type;
var value = opts.value;
var ttl = opts.ttl;
var priority = (opts.priority || '');
var weight = (opts.weight || '');
var port = (opts.port || '');
return OAUTH3.request({
url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub
+ '/dns/' + tld + '/' + sld + '/' + sub
, method: 'POST'
, session: session
, data: [{
type: type
, value: value
, ttl: ttl
, priority: priority
, weight: weight
, port: port
}]
}, {}).then(function (res) {
return res.data || res;
});
};
OAUTH3.api['dns.unset'] = function (providerUri, opts) {
var session = opts.session;
var tld = opts.tld;
var sld = opts.sld;
var sub = (opts.sub || '@');
var type = opts.type;
var value = opts.value;
return OAUTH3.request({
url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub
+ '/dns/' + tld + '/' + sld + '/' + sub + '/' + type + '/' + value
, method: 'DELETE'
, session: session
}, {}).then(function (res) {
return res.data || res;
});
};
}('undefined' !== typeof exports ? exports : window)); }('undefined' !== typeof exports ? exports : window));

View File

@ -3,35 +3,6 @@
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
OAUTH3.api['domains.checkAvailability'] = function (providerUri, opts) {
var session = opts.session;
var sld = opts.sld;
var tld = opts.tld;
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/check-availability/' + sld + '/' + tld
, session: session
}).then(function (res) {
return res;
});
};
OAUTH3.api['domains.purchase'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/registrations'
, session: session
, data: opts.data
}).then(function (res) {
return res;
});
};
OAUTH3.api['domains.list'] = function (providerUri, opts) { OAUTH3.api['domains.list'] = function (providerUri, opts) {
var session = opts.session; var session = opts.session;
@ -45,102 +16,4 @@ OAUTH3.api['domains.list'] = function (providerUri, opts) {
}); });
}; };
// TODO: Manual Renew Function
OAUTH3.api['domains.extend'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/registrations/' + opts.data.tld + '/' + opts.data.sld + '/extend'
, session: session
, data: opts.data
}).then(function (res) {
return res;
});
};
OAUTH3.api['ns.list'] = function (providerUri, opts) {
var session = opts.session;
var domain = opts.domain;
var nameArr = domain.split('.');
var reverseNameArr = nameArr.reverse();
var nameSubArr = reverseNameArr.slice(3);
var tld;
var sld;
var sub;
if (reverseNameArr[0] === 'me' && reverseNameArr[1] === 'daplie') {
tld = 'daplie.me';
sld = reverseNameArr[2];
sub = nameSubArr.reverse().join('.') || '';
} else {
tld = nameArr[0];
sld = nameArr[1];
sub = reverseNameArr.slice(2).reverse().join('.') || '';
}
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/ns/'
+ tld + '/' + sld + '/' + sub
, session: session
}).then(function (res) {
return res.data;
});
};
OAUTH3.api['ns.add'] = function (providerUri, opts) {
var session = opts.session;
var server = opts.server;
var tld = opts.tld;
var sld = opts.sld;
var sub = opts.sub;
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/ns/'
+ tld + '/' + sld + '/' + sub
, session: session
, data: { nameservers: [server] }
}).then(function (res) {
return res;
});
};
OAUTH3.api['glue.list'] = function (providerUri, opts) {
var session = opts.session;
return OAUTH3.request({
method: 'GET'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/glue'
, session: session
}).then(function (res) {
return res.data;
});
};
OAUTH3.api['glue.add'] = function (providerUri, opts) {
var session = opts.session;
var ip = opts.ip;
var tld = opts.tld;
var sld = opts.sld;
var sub = (opts.sub || '@');
return OAUTH3.request({
method: 'POST'
, url: OAUTH3.url.normalize(providerUri)
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/glue/'
+ tld + '/' + sld + '/' + sub
, session: session
, data: { ip: ip }
}, {}).then(function (res) {
return res;
});
};
}('undefined' !== typeof exports ? exports : window)); }('undefined' !== typeof exports ? exports : window));

View File

@ -3,6 +3,39 @@
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; 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) { OAUTH3.url.parse = function (url) {
// TODO browser // TODO browser
// Node should replace this // Node should replace this
@ -25,16 +58,8 @@ OAUTH3.url._isRedirectHostSafe = function (referrerUrl, redirectUrl) {
}; };
OAUTH3.url.checkRedirect = function (client, query) { OAUTH3.url.checkRedirect = function (client, query) {
console.warn("[security] URL path checking not yet implemented"); 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 var clientUrl = OAUTH3.url.normalize(client.url);
// 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 redirectUrl = OAUTH3.url.normalize(query.redirect_uri); var redirectUrl = OAUTH3.url.normalize(query.redirect_uri);
// General rule: // General rule:
@ -47,18 +72,6 @@ OAUTH3.url.checkRedirect = function (client, query) {
return true; 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; return false;
}; };
OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) { OAUTH3.url.redirect = function (clientParams, grants, tokenOrError) {
@ -97,11 +110,13 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
// Example Resource Owner Password Request // Example Resource Owner Password Request
// (generally for 1st party and direct-partner mobile apps, and webapps) // (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>>" // { "grant_type": "password", "client_id": "<<id>>", "scope": "<<scope>>"
// , "username": "<<username>>", "password": "password" } // , "username": "<<username>>", "password": "password" }
// //
opts = opts || {}; opts = opts || {};
var type = 'access_token';
var grantType = 'password';
if (!opts.password) { if (!opts.password) {
if (opts.otp) { 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; var otpCode = opts.otp || opts.otpCode || opts.otp_code || opts.otpToken || opts.otp_token || undefined;
// TODO require user agent // TODO require user agent
var params = { var params = {
client_id: opts.client_id || opts.client_uri client_id: opts.client_id || opts.client_uri
, client_uri: opts.client_uri , client_uri: opts.client_uri
, grant_type: 'password' , grant_type: grantType
, username: opts.username , username: opts.username
, password: opts.password || otpCode || undefined , password: opts.password || otpCode || undefined
, totp: opts.totp || opts.totpToken || opts.totp_token || 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 //, "jwt": opts.jwt // TODO sign a proof with a previously loaded public_key
, debug: opts.debug || undefined , debug: opts.debug || undefined
}; };
var uri = args.url;
var body;
if (opts.totp) {
params.totp = opts.totp;
}
if (opts.client_uri) { if (clientUri) {
params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos; params.clientAgreeTos = clientAgreeTos;
if (!params.clientAgreeTos) { if (!clientAgreeTos) {
throw new Error('Developer Error: missing clientAgreeTos uri'); throw new Error('Developer Error: missing clientAgreeTos uri');
} }
} }
var scope = opts.scope || directive.authn_scope;
if (scope) { if (scope) {
params.scope = OAUTH3.scope.stringify(scope); params.scope = OAUTH3.scope.stringify(scope);
} }
var uri = args.url;
var body;
if ('GET' === args.method.toUpperCase()) { if ('GET' === args.method.toUpperCase()) {
uri += '?' + OAUTH3.query.stringify(params); uri += '?' + OAUTH3.query.stringify(params);
} else { } else {
@ -153,7 +173,7 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
} }
return { return {
url: OAUTH3.url.resolve(directive.api, uri) url: OAUTH3.url.resolve(directive.issuer, uri)
, method: args.method , method: args.method
, data: body , data: body
}; };
@ -161,10 +181,6 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
OAUTH3.urls.grants = function (directive, opts) { OAUTH3.urls.grants = function (directive, opts) {
// directive = { issuer, authorization_decision } // directive = { issuer, authorization_decision }
// opts = { response_type, scopes{ granted, requested, pending, accepted } } // opts = { response_type, scopes{ granted, requested, pending, accepted } }
var grantsDir = directive.grants;
if (!grantsDir) {
throw new Error("provider doesn't support grants");
}
if (!opts) { if (!opts) {
throw new Error("You must supply a directive and an options object."); 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"); console.warn("You should supply options.referrer");
} }
if (!opts.method) { if (!opts.method) {
console.warn("You should supply options.method as either 'GET', or 'POST'"); console.warn("You must supply options.method as either 'GET', or 'POST'");
opts.method = grantsDir.method || 'GET';
} }
if ('POST' === opts.method) { if ('POST' === opts.method) {
if ('string' !== typeof opts.scope) { 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) { if (-1 === ['token', 'code'].indexOf(opts.response_type)) {
console.log("provide 'sub' to urls.grants to specify the PPID for the client"); 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.issuer, directive.grants.url)
.replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED') .replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri))
.replace(/(:azp|:client_id)/g, !opts.all && OAUTH3.uri.normalize(opts.client_id || opts.client_uri) || '') .replace(/(:sub|:account_id)/g, opts.session.token.sub)
.replace(/\/\/$/, '/') // if there's a double slash due to the sub not existing
; ;
var data = { var data = {
client_id: opts.client_id client_id: opts.client_id
, client_uri: opts.client_uri , client_uri: opts.client_uri
, referrer: opts.referrer , referrer: opts.referrer
, response_type: opts.response_type
, scope: opts.scope , scope: opts.scope
, sub: opts.sub , tenant_id: opts.tenant_id
}; };
var body; var body;
if ('GET' === opts.method) { if ('GET' === opts.method) {
url += '?' + OAUTH3.query.stringify(data); url += '?' + OAUTH3.query.stringify(data);
} else { }
else {
body = data; body = data;
} }
@ -218,98 +234,23 @@ OAUTH3.urls.grants = function (directive, opts) {
, session: opts.session , 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) { OAUTH3.authn = {};
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.loginMeta = function (directive, opts) { OAUTH3.authn.loginMeta = function (directive, opts) {
var url = OAUTH3.urls.credentialMeta(directive, opts);
return OAUTH3.request({ return OAUTH3.request({
method: directive.credential_meta.method || 'GET' method: directive.credential_meta.method || 'GET'
// TODO lint urls // TODO lint urls
// TODO client_uri // TODO client_uri
, url: url , url: OAUTH3.url.resolve(directive.issuer, 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 // TODO client_uri
return { var preq = {
method: directive.credential_otp.method || 'POST' method: directive.credential_otp.method || 'POST'
, url: OAUTH3.url.resolve(directive.api, directive.credential_otp.url) , url: OAUTH3.url.resolve(directive.issuer, directive.credential_otp.url)
, data: { , data: {
// TODO replace with signed hosted file // TODO replace with signed hosted file
client_agree_tos: 'oauth3.org/tos/draft' client_agree_tos: 'oauth3.org/tos/draft'
@ -320,17 +261,20 @@ OAUTH3.urls.otp = function (directive, opts) {
, username: opts.email , username: opts.email
} }
}; };
};
OAUTH3.authn.otp = function (directive, opts) {
var preq = OAUTH3.urls.otp(directive, opts);
return OAUTH3.request(preq); return OAUTH3.request(preq);
}; };
OAUTH3.authn.resourceOwnerPassword = function (directive, opts) { OAUTH3.authn.resourceOwnerPassword = function (directive, opts) {
var providerUri = directive.issuer; var providerUri = directive.issuer;
return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) { //var scope = opts.scope;
var data = resp.data; //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; data.provider_uri = providerUri;
if (data.error) { if (data.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); 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 } opts.session || { provider_uri: providerUri, client_uri: opts.client_uri || opts.clientUri }
, data , 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 = {};
OAUTH3.authz.scopes = function (providerUri, session, clientParams) { 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 clientUri = OAUTH3.uri.normalize(clientParams.client_uri || OAUTH3._browser.window.document.referrer);
var scope = clientParams.scope || 'authn@oauth3.org'; var scope = clientParams.scope || '';
if ('authn@oauth3.org' === scope.toString()) { var clientObj = clientParams;
// 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);
}
return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) { if (!scope) {
if (granted) { scope = 'oauth3_authn';
if (typeof granted.scope === 'string') {
return OAUTH3.scope.parse(granted.scope);
} else if (Array.isArray(granted.scope)) {
return granted.scope;
}
} }
return OAUTH3.authz.grants(providerUri, { return OAUTH3.authz.grants(providerUri, {
@ -393,31 +305,74 @@ OAUTH3.authz.scopes = function (providerUri, session, clientParams) {
, client_id: clientUri , client_id: clientUri
, client_uri: clientUri , client_uri: clientUri
, session: session , session: session
}).then(function (results) { }).then(function (grantResults) {
return results.grants; var grants;
}, function (err) { var grantedScopes;
if (!/no .*grants .*found/i.test(err.message)) { var grantedScopesMap;
throw err; 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 []; location.href = callbackUrl;
}); return;
}).then(function (granted) { }
var requested = OAUTH3.scope.parse(scope);
var accepted = []; if ('oauth3_authn' === scope) {
var pending = []; // implicit ppid grant is automatic
requested.forEach(function (scp) { console.warn('[security] fix scope checking on backend so that we can do automatic grants');
if (granted.indexOf(scp) < 0) { // TODO check user preference if implicit ppid grant is allowed
pending.push(scp); //return generateToken(session, clientObj);
} else { }
accepted.push(scp);
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 { return {
requested: requested // all requested, now pending: pendingScopes // not yet accepted
, granted: granted // all granted, ever , granted: grantedScopes // all granted, ever
, accepted: accepted // intersection of granted (ever) and requested (now) , requested: scopes // all requested, now
, pending: pending // not yet accepted , accepted: acceptedScopes // granted (ever) and requested (now)
}; };
}); });
}; };
@ -426,406 +381,144 @@ OAUTH3.authz.grants = function (providerUri, opts) {
client_id: providerUri client_id: providerUri
, debug: opts.debug , debug: opts.debug
}).then(function (directive) { }).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; var grants = grantsResult.originalData || grantsResult.data;
// TODO
if (grants.error) { if (grants.error) {
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, grants)); 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 { return {
client: grants.azp client: OAUTH3.hooks.grants.get(opts.client_id + '-client')
, clientSub: grants.azpSub , grants: OAUTH3.hooks.grants.get(opts.client_id) || []
, grants: OAUTH3.scope.parse(grants.scope)
}; };
}); });
});
}; };
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) { 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; scopes.new = scopes.new || [];
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();
}
return prom.then(function () { if ('token' === clientParams.response_type) {
return OAUTH3.hooks.keyPairs.get(session.token.sub); // get token and redirect client-side
}).then(function (keyPair) { return OAUTH3.authz.grants(providerUri, {
if (!keyPair) {
return OAUTH3.discover(providerUri, {
client_id: providerUri
, debug: clientParams.debug
}).then(function (directive) {
return OAUTH3.request(OAUTH3.urls.clientToken(directive, {
method: 'POST' method: 'POST'
, session: session
, referrer: clientParams.referrer
, response_type: clientParams.response_type
, client_id: clientParams.client_uri , client_id: clientParams.client_uri
, azp: clientParams.client_uri , client_uri: clientParams.client_uri
, aud: clientParams.aud , scope: scopes.granted.concat(scopes.new).join(',')
, exp: clientParams.exp , response_type: clientParams.response_type
, refresh_token: clientParams.refresh_token , referrer: clientParams.referrer
, refresh_exp: clientParams.refresh_exp , session: session
, debug: clientParams.debug , debug: clientParams.debug
})).then(function (result) { }).then(function (results) {
return result.originalData || result.data;
}); OAUTH3.url.redirect(clientParams, scopes, results);
}); });
} }
else if ('code' === clientParams.response_type) {
return OAUTH3.hooks.grants.get(keyPair.sub, clientParams.client_uri).then(function (grant) { // get token and redirect server-side
var now = Math.floor(Date.now()/1000); // (requires insecure form post as per spec)
var payload = { //OAUTH3.requests.authorizationDecision();
iat: now window.alert("Authorization Code Redirect NOT IMPLEMENTED");
, iss: providerUri throw new Error("Authorization Code Redirect NOT IMPLEMENTED");
, 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});
});
};
OAUTH3.requests = {}; OAUTH3.requests = {};
//OAUTH3.accounts = {};
OAUTH3.requests.accounts = {}; OAUTH3.requests.accounts = {};
OAUTH3.urls.accounts = {}; OAUTH3.requests.accounts.update = function (directive, session, opts) {
OAUTH3.urls.accounts._ = function (directives, directive, session, opts) { var dir = directive.update_account || {
opts = opts || {}; method: 'POST'
var dir = directive || { , url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId'
//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'
, bearer: 'Bearer' , bearer: 'Bearer'
}; };
var url = dir.url var url = dir.url
.replace(/:accountId/, opts.accountId || '') .replace(/:accountId/, opts.accountId)
.replace(/\/$/, '')
; ;
return { return OAUTH3.request({
url: url method: dir.method || 'POST'
//, method: dir.method || 'POST' , url: url
, session: session
/*
, headers: { , headers: {
'Authorization': (dir.bearer || 'Bearer') + ' ' + (session.access_token || session.accessToken) 'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken
} }
*/ , json: {
};
};
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 = {
name: opts.name name: opts.name
, comment: opts.comment , comment: opts.comment
, displayName: opts.displayName , displayName: opts.displayName
, priority: opts.priority , priority: opts.priority
}
});
}; };
return urlObj; OAUTH3.requests.accounts.create = function (directive, session, account) {
var dir = directive.create_account || {
method: 'POST'
, url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts'
, bearer: 'Bearer'
}; };
OAUTH3.urls.accounts.create = function (directives, session, account) { var data = {
var urlObj = OAUTH3.urls.accounts._(directives, directives.create_account, session); // TODO fix the server to just use one scheme
var profile = { // account = { nick, self: { comment, username } }
// account = { name, comment, display_name, priority }
account: {
nick: account.display_name 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 , name: account.name
, comment: account.comment , comment: account.comment
, display_name: account.display_name , display_name: account.display_name
, priority: account.priority , priority: account.priority
}; , self: {
var credentials = [ { token: session.access_token } ]; nick: account.display_name
urlObj.method = (directives.create_account || { method: 'POST' }).method; , name: account.name
urlObj.json = { , comment: account.comment
// TODO fix the server to just use one scheme , display_name: account.display_name
// account = { nick, self: { comment, username } } , priority: account.priority
// account = { name, comment, display_name, priority } }
credentials: credentials }
, profile: profile , logins: [
// 'account' is deprecated in favor of 'profile' {
, account: profile token: session.access_token
// '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 = { OAUTH3.hooks.grants = {
get: function (id, clientUri) { // Provider Only
OAUTH3.hooks._checkStorage('grants', 'get'); set: function (clientUri, newGrants) {
clientUri = OAUTH3.uri.normalize(clientUri);
if (!id) { console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
throw new Error("id is not set"); console.warn(newGrants);
if (!this._cache) { this._cache = {}; }
console.log('clientUri, newGrants');
console.log(clientUri, newGrants);
this._cache[clientUri] = newGrants;
return newGrants;
} }
if (!clientUri) { , get: function (clientUri) {
throw new Error("clientUri is not set"); clientUri = OAUTH3.uri.normalize(clientUri);
} console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.get(id, OAUTH3.uri.normalize(clientUri))); if (!this._cache) { this._cache = {}; }
} console.log('clientUri, existingGrants');
, set: function (id, clientUri, grants) { console.log(clientUri, this._cache[clientUri]);
OAUTH3.hooks._checkStorage('grants', 'set'); return this._cache[clientUri];
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();
} }
}; };

View File

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

View File

@ -1,12 +1,13 @@
;(function () { ;(function () {
'use strict'; 'use strict';
var modules = { angular
azp: [ .module('org.oauth3', [])
.service('Oauth3', [
'$timeout' '$timeout'
, '$q' , '$q'
, '$rootScope' , function Oauth3($timeout, $q) {
, function Oauth3($timeout, $q, $rootScope) {
var OAUTH3 = window.OAUTH3; var OAUTH3 = window.OAUTH3;
// We need to make angular's $q appear to be a standard Promise/A+ // We need to make angular's $q appear to be a standard Promise/A+
@ -29,20 +30,9 @@ var modules = {
PromiseAngularQ.all = $q.all; PromiseAngularQ.all = $q.all;
OAUTH3.PromiseA = PromiseAngularQ; OAUTH3.PromiseA = PromiseAngularQ;
OAUTH3._digest = function () {
$rootScope.$digest();
};
window.ngOauth3 = OAUTH3; window.ngOauth3 = OAUTH3;
return OAUTH3; return OAUTH3;
} }]);
]
};
angular
.module('oauth3.org', [])
.service('azp@oauth3.org', modules.azp)
.service('AzpOauth3', modules.azp)
;
}()); }());

View File

@ -28,7 +28,6 @@ OAUTH3._base64.atob = function (base64) {
OAUTH3._base64.btoa = function (text) { OAUTH3._base64.btoa = function (text) {
return new Buffer(text, 'utf8').toString('base64'); return new Buffer(text, 'utf8').toString('base64');
}; };
OAUTH3._defaultStorage = require('./oauth3.node.storage');
OAUTH3._node = {}; OAUTH3._node = {};
OAUTH3._node.discover = function(providerUri/*, opts*/) { OAUTH3._node.discover = function(providerUri/*, opts*/) {
@ -44,7 +43,6 @@ OAUTH3._node.request = function(preq/*, _sys*/) {
method: preq.method method: preq.method
, url: preq.url || preq.uri , url: preq.url || preq.uri
, headers: preq.headers , headers: preq.headers
, timeout: preq.timeout || undefined
, json: preq.data || preq.body || preq.json || undefined // TODO which to use? , json: preq.data || preq.body || preq.json || undefined // TODO which to use?
, formData: preq.formData || undefined , formData: preq.formData || undefined
}; };
@ -61,7 +59,10 @@ OAUTH3._node._parseJson = function (resp) {
// TODO toCamelCase // TODO toCamelCase
if (!(resp.statusCode >= 200 && resp.statusCode < 400)) { if (!(resp.statusCode >= 200 && resp.statusCode < 400)) {
// console.log('[A3] DEBUG', resp.body);
err = new Error("bad response code: " + resp.statusCode); err = new Error("bad response code: " + resp.statusCode);
err.result = resp.body;
return PromiseA.reject(err);
} }
//console.log('resp.body', typeof resp.body); //console.log('resp.body', typeof resp.body);
@ -69,16 +70,15 @@ OAUTH3._node._parseJson = function (resp) {
try { try {
json = JSON.parse(json); json = JSON.parse(json);
} catch(e) { } catch(e) {
err = err || (new Error('response not parsable: ' + resp.body)); err = new Error('response not parsable:' + resp.body);
err.result = resp.body;
return PromiseA.reject(err);
} }
} }
// handle both Oauth2- and node-style errors // handle both Oauth2- and node-style errors
if (json && json.error) { if (json.error) {
err = new Error(json.error.message || json.error_description || JSON.stringify(json.error)); err = new Error(json.error && json.error.message || json.error_description || json.error);
}
if (err) {
err.result = json; err.result = json;
return PromiseA.reject(err); return PromiseA.reject(err);
} }

View File

@ -1,137 +1,46 @@
'use strict'; 'use strict';
var PromiseA = require('bluebird'); var fs = require('fs');
var fs = PromiseA.promisifyAll(require('fs'));
var path = require('path'); var path = require('path');
//var oauth3dir = process.cwd();
var oauth3dir = path.join(require('os').homedir(), '.oauth3', 'v1');
var sessionsdir = path.join(oauth3dir, 'sessions');
var directivesdir = path.join(oauth3dir, 'directives');
var metadir = path.join(oauth3dir, 'meta');
// We can reasonably assume the existence of the home directory, but we can't assume
// that there will already be a `.oauth3` directory or anything inside of it.
if (!fs.existsSync(path.join(oauth3dir, '..'))) {
fs.mkdirSync(path.join(oauth3dir, '..'));
}
if (!fs.existsSync(oauth3dir)) {
fs.mkdirSync(oauth3dir);
}
if (!fs.existsSync(directivesdir)) {
fs.mkdirSync(directivesdir);
}
if (!fs.existsSync(sessionsdir)) {
fs.mkdirSync(sessionsdir);
}
if (!fs.existsSync(metadir)) {
fs.mkdirSync(metadir);
}
module.exports = { module.exports = {
directives: { directives: {
all: function () { get: function (providerUri) {
return fs.readdirAsync(directivesdir).then(function (nodes) {
return nodes.map(function (node) {
try {
return require(path.join(directivesdir, node));
} catch(e) {
return null;
}
}).filter(Boolean);
});
}
, get: function (providerUri) {
// TODO make safe // TODO make safe
try { try {
return require(path.join(directivesdir, providerUri + '.json')); return require(path.join(process.cwd(), providerUri + '.directives.json'));
} catch(e) { } catch(e) {
return null; return null;
} }
} }
, set: function (providerUri, directives) { , set: function (providerUri, directives) {
return fs.writeFileAsync( fs.writeFileSync(path.join(process.cwd(), providerUri + '.directives.json'), JSON.stringify(directives, null, 2));
path.join(directivesdir, providerUri + '.json')
, JSON.stringify(directives, null, 2)
).then(function () {
return directives; return directives;
});
}
, clear: function () {
return fs.readdirAsync(directivesdir).then(function (nodes) {
return PromiseA.all(nodes.map(function (node) {
return fs.unlinkAsync(path.join(directivesdir, node)).then(function () { }, function () { });
}));
});
} }
} }
, sessions: { , sessions: {
all: function (providerUri) { get: function (providerUri, id) {
return fs.readdirAsync(sessionsdir).then(function (nodes) {
return nodes.map(function (node) {
var result = require(path.join(sessionsdir, node));
if (result.link) {
return null;
}
}).filter(Boolean).filter(function (result) {
if (!providerUri || providerUri === result.issuer) {
return result;
}
});
});
}
, get: function (providerUri, id) {
var result;
try {
if (id) {
return PromiseA.resolve(require(path.join(sessionsdir, providerUri + '.' + id + '.json')));
}
else {
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));
}
}
} catch(e) {
return PromiseA.resolve(null);
}
return PromiseA.resolve(result);
}
, set: function (providerUri, session, id) {
var p;
if (id) {
p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.' + id + '.json'), JSON.stringify(session, null, 2));
}
else {
p = fs.writeFileAsync(path.join(sessionsdir, providerUri + '.json'), JSON.stringify(session, null, 2));
}
return p.then(function () {
return session;
});
}
, clear: function () {
return fs.readdirAsync(sessionsdir).then(function (nodes) {
return PromiseA.all(nodes.map(function (node) {
return fs.unlinkAsync(path.join(sessionsdir, node));
}));
});
}
}
, meta: {
get: function (key) {
// TODO make safe // TODO make safe
try { try {
return PromiseA.resolve(require(path.join(metadir, key + '.json'))); if (id) {
return require(path.join(process.cwd(), providerUri + '.' + id + '.session.json'));
}
else {
return require(path.join(process.cwd(), providerUri + '.session.json'));
}
} catch(e) { } catch(e) {
return PromiseA.resolve(null); return null;
} }
} }
, set: function (key, value) { , set: function (providerUri, session, id) {
return fs.writeFileAsync(path.join(metadir, key + '.json'), JSON.stringify(value, null, 2)).then(function () { if (id) {
return value; fs.writeFileSync(path.join(process.cwd(), providerUri + '.' + id + '.session.json'), JSON.stringify(session, null, 2));
}); }
else {
fs.writeFileSync(path.join(process.cwd(), providerUri + '.session.json'), JSON.stringify(session, null, 2));
}
return session;
} }
} }
}; };

View File

@ -0,0 +1,66 @@
{
"provider_uri": "https://oauth3.org",
"client_uri": "oauth3.org",
"token_type": "bearer",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIzOTZjMzJlNzE1NmIzNGI1ZWY1ZWYwZWU4Zjk3Y2NiMyIsImlhdCI6MTQ5MDE5NzUyNywiaXNzIjoib2F1dGgzLm9yZyIsImF1ZCI6Im9hdXRoMy5vcmciLCJhenAiOiJvYXV0aDMub3JnIiwic3ViIjoiIiwia2lkIjoib2F1dGgzLm9yZyIsInNjcCI6IiIsImFzIjoibG9naW4iLCJncnQiOiJwYXNzd29yZCIsInNydiI6ZmFsc2UsImsiOiJvYXV0aDMub3JnIiwiYXBwIjoib2F1dGgzLm9yZyIsImFjeCI6eyJpZCI6IjE1LUxhM3JnZXBFelBCR0xITHlrdEZOT1NDZFNVOXZJdjlKc2EzTkMxYVJUc3ZmUTZ5cDJuVFFfZWxmdkhzYTEifSwiYXhzIjpbXSwidXNyIjoiYTM3YWVkYTk5ZDQ5MThhMDM0YzM0MmQ2NGNkZjRiN2VkMjM0ZGZlNSIsImFjYyI6eyJpZCI6IjUzZTUwMTk2LTE4ZTMtNGJlNi04NDcyLTQ1ZDBjNDMxZjdhZCJ9LCJhY3MiOltdLCJpZHgiOiJuZnZ1bHRETE0tT0EzVUV3dVJHTDE3RFY1UXpIbWhac005Z2xMdnFLVGJacGh1T0NqMnBEUzByRk9XSXhaRjZLIiwidG9rZW5UeXBlIjoiYmVhcmVyIiwiZXhwIjoxNDkwMTk5MzI3LCJpcCI6IjIwNy4xNzMuMTY1LjUwIn0.qQu6NdsU4oVucv4uV_jusfL2HKgnPpfwF6iVG0H-P08akDtGgDoXcyVfl6hQdpVL9DGYVwvCPPUkLT0bJztM08lWhg69dVs-2e2I2BhjClsKeLsrFDBrUMwWVqqzCNVj8WBzcULLtl_mEgZc1qwVpZvXXiu0vmrRl3gtzVRaLL0",
"scope": "",
"expires_in": 1800,
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI0OGJhNjJhNTQxNGFlODQ3OWJhMzA0MGQ1Mzc5NmY3MiIsImlhdCI6MTQ5MDE5NzUyNywiaXNzIjoib2F1dGgzLm9yZyIsImF1ZCI6Im9hdXRoMy5vcmciLCJhenAiOiJvYXV0aDMub3JnIiwic3ViIjoiIiwia2lkIjoib2F1dGgzLm9yZyIsInNjcCI6IiIsImFzIjoibG9naW4iLCJncnQiOiJwYXNzd29yZCIsInNydiI6ZmFsc2UsImsiOiJvYXV0aDMub3JnIiwiYXBwIjoib2F1dGgzLm9yZyIsImFjeCI6eyJpZCI6IjE1LUxhM3JnZXBFelBCR0xITHlrdEZOT1NDZFNVOXZJdjlKc2EzTkMxYVJUc3ZmUTZ5cDJuVFFfZWxmdkhzYTEifSwiYXhzIjpbXSwidXNyIjoiYTM3YWVkYTk5ZDQ5MThhMDM0YzM0MmQ2NGNkZjRiN2VkMjM0ZGZlNSIsImFjYyI6eyJpZCI6IjUzZTUwMTk2LTE4ZTMtNGJlNi04NDcyLTQ1ZDBjNDMxZjdhZCJ9LCJhY3MiOltdLCJpZHgiOiJuZnZ1bHRETE0tT0EzVUV3dVJHTDE3RFY1UXpIbWhac005Z2xMdnFLVGJacGh1T0NqMnBEUzByRk9XSXhaRjZLIiwicmVmcmVzaCI6dHJ1ZX0.QfufVyAGit2YOy9Hs9mv4eoCuyCYb9FDT_UXGd3JaFZe6MwqxLLnq2fWkkV2jgzDAK5t0JMu2Vk91jPP2IBXMkpZSzjaEKJ3-Eokb14Mo5GIrp54ndM20gWVZc-ReQtOUtSVG28bfnOBT5ceUM6SBrTxfz1ENOfmAiWl5591roQ",
"token": {
"jti": "396c32e7156b34b5ef5ef0ee8f97ccb3",
"iat": 1490197527,
"iss": "oauth3.org",
"aud": "oauth3.org",
"azp": "oauth3.org",
"sub": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1",
"kid": "oauth3.org",
"scp": "",
"as": "login",
"grt": "password",
"srv": false,
"k": "oauth3.org",
"app": "oauth3.org",
"acx": {
"id": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1"
},
"axs": [],
"usr": "a37aeda99d4918a034c342d64cdf4b7ed234dfe5",
"acc": {
"id": "53e50196-18e3-4be6-8472-45d0c431f7ad"
},
"acs": [],
"idx": "nfvultDLM-OA3UEwuRGL17DV5QzHmhZsM9glLvqKTbZphuOCj2pDS0rFOWIxZF6K",
"tokenType": "bearer",
"exp": 1490199327,
"ip": "207.173.165.50",
"client_uri": "oauth3.org",
"provider_uri": "https://oauth3.org"
},
"refresh": {
"jti": "48ba62a5414ae8479ba3040d53796f72",
"iat": 1490197527,
"iss": "oauth3.org",
"aud": "oauth3.org",
"azp": "oauth3.org",
"sub": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1",
"kid": "oauth3.org",
"scp": "",
"as": "login",
"grt": "password",
"srv": false,
"k": "oauth3.org",
"app": "oauth3.org",
"acx": {
"id": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1"
},
"axs": [],
"usr": "a37aeda99d4918a034c342d64cdf4b7ed234dfe5",
"acc": {
"id": "53e50196-18e3-4be6-8472-45d0c431f7ad"
},
"acs": [],
"idx": "nfvultDLM-OA3UEwuRGL17DV5QzHmhZsM9glLvqKTbZphuOCj2pDS0rFOWIxZF6K",
"refresh": true,
"provider_uri": "https://oauth3.org"
}
}

66
oauth3.org.session.json Normal file
View File

@ -0,0 +1,66 @@
{
"provider_uri": "https://oauth3.org",
"client_uri": "oauth3.org",
"token_type": "bearer",
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI4MmU5MDhlMTljMmIxM2IxNmM3N2JlMTNkMTljZmEzOSIsImlhdCI6MTQ5MDEzODg2MiwiaXNzIjoib2F1dGgzLm9yZyIsImF1ZCI6Im9hdXRoMy5vcmciLCJhenAiOiJvYXV0aDMub3JnIiwic3ViIjoiIiwia2lkIjoib2F1dGgzLm9yZyIsInNjcCI6IiIsImFzIjoibG9naW4iLCJncnQiOiJwYXNzd29yZCIsInNydiI6ZmFsc2UsImsiOiJvYXV0aDMub3JnIiwiYXBwIjoib2F1dGgzLm9yZyIsImFjeCI6eyJpZCI6IjE1LUxhM3JnZXBFelBCR0xITHlrdEZOT1NDZFNVOXZJdjlKc2EzTkMxYVJUc3ZmUTZ5cDJuVFFfZWxmdkhzYTEifSwiYXhzIjpbXSwidXNyIjoiYTM3YWVkYTk5ZDQ5MThhMDM0YzM0MmQ2NGNkZjRiN2VkMjM0ZGZlNSIsImFjYyI6eyJpZCI6IjUzZTUwMTk2LTE4ZTMtNGJlNi04NDcyLTQ1ZDBjNDMxZjdhZCJ9LCJhY3MiOltdLCJpZHgiOiJuZnZ1bHRETE0tT0EzVUV3dVJHTDE3RFY1UXpIbWhac005Z2xMdnFLVGJacGh1T0NqMnBEUzByRk9XSXhaRjZLIiwidG9rZW5UeXBlIjoiYmVhcmVyIiwiZXhwIjoxNDkwMTQwNjYyLCJpcCI6IjIwNy4xNzMuMTY1LjUwIn0.luWnALBIv9TD_mGHUIddRpOnbVAhkYO-DJtEQitODQX2IEC7cIcbSrvJBKI3i_djeMj69fm-ctr6XFUU7WiEVnBsMh55WK1gkdFqFzImo67apQ5kAV8GTGGbG___kisjl12AMvL09_shU1Sp8F8cHayTZTmSbyyWbbFKT3cZCsg",
"scope": "",
"expires_in": 1800,
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI1ZDBjMGMyNTkyNmZhNmMxOTM0MjlhZWRkMjJjOTEyYiIsImlhdCI6MTQ5MDEzODg2MiwiaXNzIjoib2F1dGgzLm9yZyIsImF1ZCI6Im9hdXRoMy5vcmciLCJhenAiOiJvYXV0aDMub3JnIiwic3ViIjoiIiwia2lkIjoib2F1dGgzLm9yZyIsInNjcCI6IiIsImFzIjoibG9naW4iLCJncnQiOiJwYXNzd29yZCIsInNydiI6ZmFsc2UsImsiOiJvYXV0aDMub3JnIiwiYXBwIjoib2F1dGgzLm9yZyIsImFjeCI6eyJpZCI6IjE1LUxhM3JnZXBFelBCR0xITHlrdEZOT1NDZFNVOXZJdjlKc2EzTkMxYVJUc3ZmUTZ5cDJuVFFfZWxmdkhzYTEifSwiYXhzIjpbXSwidXNyIjoiYTM3YWVkYTk5ZDQ5MThhMDM0YzM0MmQ2NGNkZjRiN2VkMjM0ZGZlNSIsImFjYyI6eyJpZCI6IjUzZTUwMTk2LTE4ZTMtNGJlNi04NDcyLTQ1ZDBjNDMxZjdhZCJ9LCJhY3MiOltdLCJpZHgiOiJuZnZ1bHRETE0tT0EzVUV3dVJHTDE3RFY1UXpIbWhac005Z2xMdnFLVGJacGh1T0NqMnBEUzByRk9XSXhaRjZLIiwicmVmcmVzaCI6dHJ1ZX0.XljVX_QXgnYw8gyIZjQdxfyDlAAwWtU-kLitibw2_3xo9muLFPCL_dAk5XnMRygyyh5B9H4p4qB2Gb5BEJKJRfAtQ6TeZadTBtxwoY7zcns9f4Nx59VNii4k_Xp3uhJ6fQp8ERvkMgBy52Sj5ag0PFnuIwk35wLdSfiikDGnwKo",
"token": {
"jti": "82e908e19c2b13b16c77be13d19cfa39",
"iat": 1490138862,
"iss": "oauth3.org",
"aud": "oauth3.org",
"azp": "oauth3.org",
"sub": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1",
"kid": "oauth3.org",
"scp": "",
"as": "login",
"grt": "password",
"srv": false,
"k": "oauth3.org",
"app": "oauth3.org",
"acx": {
"id": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1"
},
"axs": [],
"usr": "a37aeda99d4918a034c342d64cdf4b7ed234dfe5",
"acc": {
"id": "53e50196-18e3-4be6-8472-45d0c431f7ad"
},
"acs": [],
"idx": "nfvultDLM-OA3UEwuRGL17DV5QzHmhZsM9glLvqKTbZphuOCj2pDS0rFOWIxZF6K",
"tokenType": "bearer",
"exp": 1490140662,
"ip": "207.173.165.50",
"client_uri": "oauth3.org",
"provider_uri": "https://oauth3.org"
},
"refresh": {
"jti": "5d0c0c25926fa6c193429aedd22c912b",
"iat": 1490138862,
"iss": "oauth3.org",
"aud": "oauth3.org",
"azp": "oauth3.org",
"sub": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1",
"kid": "oauth3.org",
"scp": "",
"as": "login",
"grt": "password",
"srv": false,
"k": "oauth3.org",
"app": "oauth3.org",
"acx": {
"id": "15-La3rgepEzPBGLHLyktFNOSCdSU9vIv9Jsa3NC1aRTsvfQ6yp2nTQ_elfvHsa1"
},
"axs": [],
"usr": "a37aeda99d4918a034c342d64cdf4b7ed234dfe5",
"acc": {
"id": "53e50196-18e3-4be6-8472-45d0c431f7ad"
},
"acs": [],
"idx": "nfvultDLM-OA3UEwuRGL17DV5QzHmhZsM9glLvqKTbZphuOCj2pDS0rFOWIxZF6K",
"refresh": true,
"provider_uri": "https://oauth3.org"
}
}

View File

@ -9,7 +9,7 @@ OAUTH3.api['tunnel.token'] = function (providerUri, opts) {
return OAUTH3.request({ return OAUTH3.request({
method: 'POST' method: 'POST'
, url: OAUTH3.url.normalize(providerUri) , 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 , session: session
, data: { , data: {
domains: opts.data.domains domains: opts.data.domains

View File

@ -1,15 +1,15 @@
{ {
"name": "oauth3.js", "name": "oauth3.js",
"version": "1.2.2", "version": "1.0.1",
"description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.", "description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.",
"main": "oauth3.node.js", "main": "oauth3.node.js",
"scripts": { "scripts": {
"prepare": "gulp", "install": "./node_modules/.bin/gulp",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@git.oauth3.org:OAuth3/oauth3.js.git" "url": "git@git.daplie.com:OAuth3/oauth3.js.git"
}, },
"keywords": [ "keywords": [
"oauth", "oauth",
@ -32,10 +32,7 @@
"sign" "sign"
], ],
"dependencies": { "dependencies": {
"bluebird": "^3.5.0", "elliptic": "^6.4.0"
"elliptic": "^6.4.0",
"request": "^2.81.0",
"terminal-forms.js": "git+https://git.oauth3.org/OAuth3/terminal-forms.js.git#v1"
}, },
"devDependencies": { "devDependencies": {
"browserify-aes": "^1.0.6", "browserify-aes": "^1.0.6",

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

@ -15,7 +15,7 @@
<!-- TODO permanently cache with appcache (or service worker?) --> <!-- TODO permanently cache with appcache (or service worker?) -->
<!-- TODO slim this all down to a single file --> <!-- TODO slim this all down to a single file -->
<script src="../../assets/oauth3.org/oauth3.core.js"></script> <script src="/assets/org.oauth3/oauth3.core.js"></script>
<script> <script>
;(function () { ;(function () {
'use strict'; 'use strict';

View File

@ -0,0 +1,12 @@
{ "terms": [ "oauth3.org/tos/draft" ]
, "authorization_dialog": { "url": "#/authorization_dialog" }
, "access_token": { "method": "POST", "url": "api/org.oauth3.provider/access_token" }
, "otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" }
, "credential_otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" }
, "credential_meta": { "url": "api/org.oauth3.provider/logins/meta/:type/:id" }
, "credential_create": { "method": "POST" , "url": "api/org.oauth3.provider/logins" }
, "grants": { "method": "GET", "url": "api/org.oauth3.provider/grants/:azp/:sub" }
, "authorization_decision": { "method": "POST", "url": "api/org.oauth3.provider/authorization_decision" }
, "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/org.oauth3/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>