Compare commits
232 Commits
Author | SHA1 | Date | |
---|---|---|---|
291bfd6a79 | |||
def91fb60d | |||
738573a79c | |||
58f245f90c | |||
c5735f402c | |||
db43f09ce2 | |||
e7ffe06d9d | |||
3d9d7b00d7 | |||
cf5c106f64 | |||
f4b3dbd495 | |||
6f6d07e670 | |||
23db17a31e | |||
d87645d135 | |||
989dbfb150 | |||
36fe8e2a80 | |||
ff6d9665e2 | |||
2587d03860 | |||
f14f42404e | |||
0aa85baf6d | |||
39c0c775ed | |||
2bfc752ea1 | |||
d7c4d0ff13 | |||
18170e94f4 | |||
ff76a4116e | |||
c9c45ebe4e | |||
d84b7bd6ea | |||
e78999d4a1 | |||
687391e56b | |||
6a131f6650 | |||
9f48a44958 | |||
be9e8852b8 | |||
d015e66f17 | |||
77c64df163 | |||
fcca994c65 | |||
e27af15485 | |||
c59e23d114 | |||
6225b1e2fe | |||
9298776620 | |||
2c0b757c13 | |||
f6017e7e49 | |||
4da61d835e | |||
215d3f0e92 | |||
bc82bb6f1b | |||
d13728dd3d | |||
|
9ac5c3ed89 | ||
c2ec21c446 | |||
a7a9a16847 | |||
82ed16e162 | |||
1e459ce186 | |||
e6fa1d5314 | |||
b7aa754c48 | |||
23d599835b | |||
0a980fd560 | |||
|
f79fa608d7 | ||
a4b7833989 | |||
|
2438081487 | ||
|
c77d280b00 | ||
|
69a8e6ec01 | ||
|
19af663368 | ||
|
bcf2f315b8 | ||
|
9a8d742ff1 | ||
|
1d3f4ca8bd | ||
|
6b0470607c | ||
|
bfd9e0ce1c | ||
|
19b0e7a5d2 | ||
|
e99b9ff4d6 | ||
|
83030fb416 | ||
|
523f57944f | ||
|
dedd851ff9 | ||
|
4d4d1af45d | ||
|
69d5cb382a | ||
|
6acb027b3a | ||
|
081b2a23de | ||
|
815ba04d37 | ||
|
d7d07b841a | ||
|
26e9a1c08b | ||
|
704337e30b | ||
|
db284fbf91 | ||
|
1fe8733a06 | ||
|
7bb0fca116 | ||
|
a5742d1a2a | ||
|
effee987be | ||
|
bf71399d12 | ||
|
5f68ea19e2 | ||
|
8961a4e519 | ||
|
6b7fd877ec | ||
|
4d80074046 | ||
|
b60e9b8fce | ||
|
9a7aa3261f | ||
|
80d462c231 | ||
|
937a681c5d | ||
|
5b11e2bca2 | ||
|
84505d1b0e | ||
|
52675f84c7 | ||
|
26ea3d931e | ||
|
c250ab07f4 | ||
|
ee631b97c7 | ||
|
dac7c3936f | ||
|
0c0b85b1af | ||
|
9c093ca3a1 | ||
|
623d94e045 | ||
|
516eda4ea6 | ||
|
5d42f3e2cc | ||
|
2cc96fef6e | ||
|
39b8e19bae | ||
|
e42079d856 | ||
|
69add2a80f | ||
|
197c0fdcb2 | ||
|
84a574e31b | ||
|
39c18ab184 | ||
|
5a5488f504 | ||
|
1993853d0d | ||
|
28dbf9ab23 | ||
|
1ca6f0a324 | ||
|
c38554a9dd | ||
|
7f1e67aaee | ||
|
e930881e0f | ||
|
3b34173ac8 | ||
|
146891a618 | ||
|
35405f8612 | ||
|
9574d9b982 | ||
|
9f9610b6f5 | ||
|
186c0ea45a | ||
|
9cb2ad036b | ||
|
1a43a58af1 | ||
|
3879129674 | ||
|
d5befcaa39 | ||
|
33a0362524 | ||
|
5163463dd2 | ||
|
4a237b0703 | ||
|
1d639dc080 | ||
|
26be6411b5 | ||
|
a3b038ffdc | ||
|
2bf75a7429 | ||
|
30d9c2e8b0 | ||
|
b9664e4e65 | ||
|
b811a242b4 | ||
|
fe62cbc5e1 | ||
|
f10dee9167 | ||
|
32609e20fa | ||
|
e39492fd4c | ||
|
7908154372 | ||
|
239980e5c2 | ||
|
fbd53e486f | ||
|
ec09adcf60 | ||
|
372f633625 | ||
|
985c65483a | ||
|
5e10e1893d | ||
|
0562b58761 | ||
|
91cd5d87fd | ||
|
45f8f640c8 | ||
|
ac47b7314d | ||
|
b4804b4c97 | ||
|
181027a07f | ||
|
90e42e13d4 | ||
|
ec33e667b3 | ||
|
672662271d | ||
|
0a0a5041b7 | ||
|
87ba1e4298 | ||
|
c4cc619928 | ||
|
5e6dc31c35 | ||
|
daa92fa829 | ||
|
0aa1f614fa | ||
|
80217fd39b | ||
|
4fd5aa05de | ||
|
7741621c16 | ||
|
652b59fad3 | ||
|
faf6814a53 | ||
|
ebb44c3dcb | ||
|
c2bb0afb67 | ||
|
a4f29edf4e | ||
|
3a805d071a | ||
|
9969c4dba9 | ||
|
f72c1a333c | ||
|
7b0d791318 | ||
|
cc3975ca0c | ||
|
e570f484bf | ||
|
66ca428bff | ||
|
eb34eb2968 | ||
|
c120e0c6cd | ||
|
3c0804a352 | ||
|
ffac789125 | ||
|
de0bc51967 | ||
|
42c4e72b57 | ||
|
6b8d08c278 | ||
|
e8c631a416 | ||
|
ddfdc0d9ba | ||
|
0bfdd009b7 | ||
|
5cb8f97056 | ||
|
516c989cc1 | ||
|
3fc02ee6f0 | ||
|
d4dd268fb4 | ||
|
1926d1e8f7 | ||
|
f505932d12 | ||
|
831fc31809 | ||
|
79ee4ba7da | ||
|
8f773c9de4 | ||
|
4bdacf9770 | ||
|
f37d47b0d4 | ||
|
dd0b257038 | ||
|
abb788780d | ||
|
c359ed6ea9 | ||
|
5ed05f03cf | ||
|
01fbffd6c2 | ||
|
5047fc1aff | ||
|
fe1b512678 | ||
|
ac89cb7904 | ||
|
9cfd517880 | ||
|
cf6c1b9e5a | ||
|
cc4af8f95a | ||
|
afb021af9b | ||
|
06411918a7 | ||
|
1008d0f6a3 | ||
|
dbd2647941 | ||
|
847e8c2a6a | ||
|
f2e6ea5890 | ||
|
695df45a1d | ||
|
01580dd6b3 | ||
|
a2c4e718fd | ||
|
d382464caf | ||
|
740c973afc | ||
|
02bb01fdf4 | ||
|
e8e9b961a4 | ||
|
6ec723ec1f | ||
|
4b63e38c1f | ||
|
bde3c2ca33 | ||
|
db9d8ff313 | ||
|
29967cde19 | ||
|
68cecb7c96 | ||
|
9ec66a10b2 | ||
|
338f62439a | ||
|
e8cafbf921 |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
prefactor
|
||||||
|
.well-known
|
||||||
|
node_modules/
|
||||||
|
DS_Store
|
||||||
|
.vscode
|
16
.jshintrc
Normal file
16
.jshintrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{ "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
|
||||||
|
}
|
5
.npmignore
Normal file
5
.npmignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.git*
|
||||||
|
browserify/
|
||||||
|
prefactor/
|
||||||
|
gulpfile.js
|
||||||
|
bump-version.sh
|
@ -1 +1 @@
|
|||||||
well-known
|
_apis
|
7
CHANGELOG
Normal file
7
CHANGELOG
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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
Normal file
41
LICENSE
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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.
|
160
README.md
160
README.md
@ -1,12 +1,18 @@
|
|||||||
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!)
|
||||||
|
|
||||||
Instead of bloating your webapp and ruining the mobile experience,
|
Instead of bloating your webapp and ruining the mobile experience,
|
||||||
you can use a single, small javascript file for all OAuth3 providers
|
you can use a single, small javascript file for all OAuth3 providers
|
||||||
(and almost all OAuth2 providers) with a seemless experience.
|
(and almost all OAuth2 providers) with a seamless experience.
|
||||||
|
|
||||||
Also, instead of complicated (or worse - insecure) CLI and Desktop login methods,
|
Also, instead of complicated (or worse - insecure) CLI and Desktop login methods,
|
||||||
you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit)
|
you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit)
|
||||||
@ -19,13 +25,12 @@ 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 `org.oauth3/`
|
3. Inside of the folder `example.com/assets` a folder called `oauth3.org/`
|
||||||
4. Download [oauth.js-v1.zip](https://git.daplie.com/Daplie/oauth3.js/repository/archive.zip?ref=v1)
|
4. Download [oauth3.js-v1.zip](https://git.oauth3.org/OAuth3/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/org.oauth3/`
|
6. Copy the file `oauth3.core.js` into the folder `example.com/assets/oauth3.org/`
|
||||||
7. Copy the folder `well-known` into the folder `example.com/`
|
7. Copy the folder `_apis` into the folder `example.com/`
|
||||||
8. Rename the folder `well-known` to `.well-known` (when you do this, it become invisible, that's okay)
|
9. Add `<script src="assets/oauth3.org/oauth3.core.js"></script>` to your `index.html`
|
||||||
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:
|
||||||
|
|
||||||
@ -44,7 +49,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/org.oauth3/oauth3.core.js"></script>
|
<script src="assets/oauth3.org/oauth3.core.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -53,15 +58,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 auth = OAUTH3.create(window.location); // use window.location to set Client URI (your app's id)
|
var oauth3 = 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 auth.setProvider(providerUri);
|
return oauth3.setIdentityProvider(providerUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -69,23 +74,25 @@ function onChangeProvider(_providerUri) {
|
|||||||
//
|
//
|
||||||
function onClickLogin() {
|
function onClickLogin() {
|
||||||
|
|
||||||
return auth.authenticate().then(function (session) {
|
return oauth3.authenticate().then(function (session) {
|
||||||
|
|
||||||
console.info('Authentication was Successful:');
|
console.info('Authentication was Successful:');
|
||||||
console.log(session);
|
console.log(session);
|
||||||
|
|
||||||
// You can use the PPID (or preferrably a hash of it) as the login for your app
|
// You can use the PPID (or preferably a hash of it) as the login for your app
|
||||||
// (it securely functions as both username and password which is known only by your app)
|
// (it securely functions as both username and password which is known only by your app)
|
||||||
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
|
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
|
||||||
//
|
//
|
||||||
console.info('Secure PPID (aka subject):', session.token.sub);
|
console.info('Secure PPID (aka subject):', session.token.sub);
|
||||||
|
|
||||||
return auth.request({
|
return oauth3.request({
|
||||||
url: 'https://oauth3.org/api/org.oauth3.provider/inspect'
|
url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid'
|
||||||
|
.replace(/:sub/g, session.token.sub)
|
||||||
|
.replace(/:kid/g, session.token.kid || session.token.iss)
|
||||||
, session: session
|
, session: session
|
||||||
}).then(function (resp) {
|
}).then(function (resp) {
|
||||||
|
|
||||||
console.info("Inspect Token:");
|
console.info("Signing Public Key JWK:");
|
||||||
console.log(resp.data);
|
console.log(resp.data);
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -102,7 +109,7 @@ function onClickLogin() {
|
|||||||
//
|
//
|
||||||
function onClickLogout() {
|
function onClickLogout() {
|
||||||
|
|
||||||
return auth.logout().then(function () {
|
return oauth3.logout().then(function () {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
||||||
console.info('Logout was Successful');
|
console.info('Logout was Successful');
|
||||||
@ -138,13 +145,13 @@ it might look like this:
|
|||||||
example.com
|
example.com
|
||||||
│
|
│
|
||||||
│
|
│
|
||||||
├── .well-known (hidden)
|
├── _apis
|
||||||
│ └── oauth3
|
│ └── oauth3.org
|
||||||
│ ├── callback.html
|
│ ├── callback.html
|
||||||
│ ├── directives.json
|
│ ├── directives.json
|
||||||
│ └── index.html
|
│ └── index.html
|
||||||
├── assets
|
├── assets
|
||||||
│ └── org.oauth3
|
│ └── oauth3.org
|
||||||
│ └── oauth3.core.js
|
│ └── oauth3.core.js
|
||||||
│
|
│
|
||||||
│
|
│
|
||||||
@ -165,17 +172,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/org.oauth3
|
# clone the project as assets/oauth3.org
|
||||||
mkdir -p assets
|
mkdir -p assets
|
||||||
git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
|
git clone git@git.oauth3.org:OAuth3/oauth3.js.git assets/oauth3.org
|
||||||
pushd assests/org.oauth3
|
pushd assets/oauth3.org
|
||||||
git checkout v1
|
git checkout v1
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|
||||||
# symlink `.well-known/oauth3` to `assets/org.oauth3/.well-known/oauth3`
|
# symlink `_apis/oauth3.org` to `assets/oauth3.org/_apis/oauth3.org`
|
||||||
mkdir -p .well-known
|
mkdir -p _apis
|
||||||
ln -sf ../assets/org.oauth3/.well-known/oauth3 .well-known/oauth3
|
ln -sf ../assets/oauth3.org/_apis/oauth3 _apis/oauth3.org
|
||||||
```
|
```
|
||||||
|
|
||||||
**Advanced Installation with `bower`**
|
**Advanced Installation with `bower`**
|
||||||
@ -185,17 +192,17 @@ ln -sf ../assets/org.oauth3/.well-known/oauth3 .well-known/oauth3
|
|||||||
bower install oauth3
|
bower install oauth3
|
||||||
|
|
||||||
|
|
||||||
# create a `.well-known` folder and an `assets` folder
|
# create a `_apis` folder and an `assets` folder
|
||||||
mkdir -p .well-known assets
|
mkdir -p _apis assets
|
||||||
|
|
||||||
|
|
||||||
# symlink `.well-known/oauth3` to `bower_components/oauth3/.well-known/oauth3`
|
# symlink `_apis/oauth3.org` to `bower_components/oauth3.org/_apis/oauth3.org`
|
||||||
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
|
ln -sf ../bower_components/oauth3.org/_apis/oauth3.org _apis/oauth3.org
|
||||||
|
|
||||||
|
|
||||||
# symlink `assets/org.oauth3` to `bower_components/oauth3`
|
# symlink `assets/oauth3.org` to `bower_components/oauth3.org`
|
||||||
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
|
ln -sf ../bower_components/oauth3.org/_apis/oauth3.org _apis/oauth3.org
|
||||||
ln -sf ../bower_components/oauth3 assets/org.oauth3
|
ln -sf ../bower_components/oauth3.org assets/oauth3.org
|
||||||
```
|
```
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
@ -204,13 +211,14 @@ 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/org.oauth3/oauth3.core.js"></script>
|
<script src="assets/oauth3.org/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
|
||||||
@ -218,7 +226,7 @@ var providerUri;
|
|||||||
//
|
//
|
||||||
function onChangeProvider(_providerUri) {
|
function onChangeProvider(_providerUri) {
|
||||||
providerUri = _providerUri;
|
providerUri = _providerUri;
|
||||||
return OAUTH3.discover(providerUri); // just to cache
|
return OAUTH3.discover(providerUri, opts); // just to cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -226,20 +234,19 @@ 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:');
|
||||||
console.log(session);
|
console.log(session);
|
||||||
|
|
||||||
// You can use the PPID (or preferrably a hash of it) as the login for your app
|
// You can use the PPID (or preferably a hash of it) as the login for your app
|
||||||
// (it securely functions as both username and password which is known only by your app)
|
// (it securely functions as both username and password which is known only by your app)
|
||||||
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
|
// If you use a hash of it as an ID, you can also use the PPID itself as a decryption key
|
||||||
//
|
//
|
||||||
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/org.oauth3.provider/inspect_token'
|
url: 'https://oauth3.org/api/issuer@oauth3.org/inspect_token'
|
||||||
, session: session
|
, session: session
|
||||||
}).then(function (resp) {
|
}).then(function (resp) {
|
||||||
|
|
||||||
@ -260,6 +267,18 @@ 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**:
|
||||||
@ -271,7 +290,19 @@ 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/org.oauth3/oauth3.ng.js"></script>
|
<script src="assets/oauth3.org/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,
|
||||||
@ -284,31 +315,48 @@ 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.
|
||||||
|
|
||||||
```
|
```
|
||||||
auth = OAUTH3.create(location); // takes a location object, such as window.location
|
oauth3 = 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 = auth.init(location); // set and fetch your own site/app's configuration details
|
promise = oauth3.init(opts); // set and fetch your own site/app's configuration details
|
||||||
// promises your site's config
|
// promises your site's config // opts = { location, session, issuer, audience }
|
||||||
|
|
||||||
promise = auth.setProvider(url); // changes the Provider URI (the site you're logging into),
|
promise = oauth3.setIdentityProvider(url); // changes the Identity Provider URI (the site you're logging into),
|
||||||
// promises the provider's config // gets the config for that site (from their .well-known/oauth3),
|
// promises the provider's config // gets the config for that site (from their _apis/oauth3.org),
|
||||||
// and caches it in internal state as the default
|
// and caches it in internal state as the default
|
||||||
|
|
||||||
promise = auth.authenticate(); // opens login window for the provider and returns a session
|
promise = oauth3.setResourceProvider(url); // changes the Resource Provider URI (the site you're getting stuff from)
|
||||||
// (must be called after the setProvider promise has completed)
|
|
||||||
|
|
||||||
promise = auth.authorize(permissions); // authenticates (if not authenticated) and opens a window to
|
promise = oauth3.setProvider(url); // changes the both Identity and Resource Provider URI together
|
||||||
|
|
||||||
|
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 = auth.request({ url, method, data }); // make an (authorized) request to a provider's resource
|
promise = oauth3.request({ url, method, data }); // make an (authorized) arbitrary request to an audience's resource
|
||||||
// (contacts, photos, whatever)
|
// (contacts, photos, whatever)
|
||||||
|
|
||||||
promise = auth.logout(); // opens logout window for the provider
|
promise = oauth3.api(apiname, opts); // make an (authorized) well-known api call to an audience
|
||||||
|
// Ex: oauth3.api('dns.list', { sld: 'example', tld: 'com' });
|
||||||
|
|
||||||
auth.session(); // returns the current session, if any
|
// TODO
|
||||||
|
api = await oauth3.package(audience, schemaname); // make an (authorized) well-known api call to an audience
|
||||||
|
// Ex: api = await oauth3.package('domains.example.com', 'dns@oauth3.org');
|
||||||
|
// 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
|
||||||
----------
|
----------
|
||||||
@ -436,10 +484,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/org.oauth3.provider
|
1. https://example.com/api/issuer@oauth3.org
|
||||||
2. example.com/api/org.oauth.provider/ (not unique)
|
2. example.com/api/issuer@oauth3.org/ (not unique)
|
||||||
3. /api/org.oauth3.provider
|
3. /api/issuer@oauth3.org
|
||||||
4. api/org.oauth3.provider (not unique)
|
4. api/issuer@oauth3.org (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.
|
||||||
@ -448,7 +496,7 @@ As a general rule I don't like rules that sometimes apply and sometimes don't,
|
|||||||
so I may need to rethink this. However, there are cases where including the protocol
|
so I may need to rethink this. However, there are cases where including the protocol
|
||||||
can be very ugly and confusing and we definitely need to allow relative paths.
|
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 (elimitate #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/.well-known/oauth3/directives.json then /api/whatever would refer
|
https://example.com/username/_apis/oauth3.org/index.json then /api/whatever would refer
|
||||||
to https://example.com/username/api/whatever.
|
to https://example.com/username/api/whatever.
|
||||||
|
BIN
_apis/oauth3/blank.gif
Normal file
BIN
_apis/oauth3/blank.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 B |
@ -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/org.oauth3/oauth3.core.js"></script>
|
<script src="../../assets/oauth3.org/oauth3.core.js"></script>
|
||||||
<script>
|
<script>
|
||||||
;(function () {
|
;(function () {
|
||||||
'use strict';
|
'use strict';
|
BIN
_apis/oauth3/clear.gif
Normal file
BIN
_apis/oauth3/clear.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 B |
12
_apis/oauth3/directives.json
Normal file
12
_apis/oauth3/directives.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{ "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/" }
|
||||||
|
}
|
140
_apis/oauth3/index.html
Normal file
140
_apis/oauth3/index.html
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<!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>
|
26
_apis/oauth3/scopes.json
Normal file
26
_apis/oauth3/scopes.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
"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"
|
||||||
|
}
|
217
bin/cli.js
Normal file
217
bin/cli.js
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
'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" ]
|
||||||
|
]
|
||||||
|
});
|
205
bin/oauth3.js
Normal file
205
bin/oauth3.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// process.stdout.isTTY
|
||||||
|
var form = require('terminal-forms.js').create(process.stdin, process.stdout);
|
||||||
|
//var dns = form.PromiseA.promisifyAll(require('dns'));
|
||||||
|
var OAUTH3 = require('../oauth3.node.js');
|
||||||
|
// TODO change to ._hooks
|
||||||
|
OAUTH3._hooks = require('../oauth3.node.storage.js');
|
||||||
|
// directives = { get, set }
|
||||||
|
// sessions = { get, set }
|
||||||
|
/*
|
||||||
|
OAUTH3._hooks.directives.get = require('../oauth3.node.storage.js').directives.get;
|
||||||
|
OAUTH3._hooks.directives.set = require('../oauth3.node.storage.js').directives.set;
|
||||||
|
OAUTH3._hooks.session.get = require('../oauth3.node.storage.js').sessions.get;
|
||||||
|
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');
|
||||||
|
//console.log('stdin tty', process.stdin.isTTY);
|
||||||
|
//console.log('stdout tty', process.stdout.isTTY);
|
||||||
|
var oauth3;
|
||||||
|
var opts = {
|
||||||
|
email: options.email
|
||||||
|
, providerUri: options.providerUri
|
||||||
|
};
|
||||||
|
if (opts.form) {
|
||||||
|
form = opts.form;
|
||||||
|
}
|
||||||
|
var email;
|
||||||
|
var providerUrl;
|
||||||
|
var providerUri;
|
||||||
|
var sameProvider;
|
||||||
|
var username;
|
||||||
|
|
||||||
|
function getSession() {
|
||||||
|
var username;
|
||||||
|
|
||||||
|
// TODO lookup uuid locally before performing loginMeta
|
||||||
|
// TODO lookup token locally before performing loginMeta / otp
|
||||||
|
return OAUTH3.authn.loginMeta(oauth3._providerDirectives, { email: email }).then(function (/*result*/) {
|
||||||
|
return { node: email, type: 'email' };
|
||||||
|
}, function (/*err*/) {
|
||||||
|
// TODO require hashcash to create user account
|
||||||
|
function confirmCreateAccount() {
|
||||||
|
// TODO directives should specify private (invite-only) vs internal (request) vs public (allow) accounts
|
||||||
|
return form.ask({
|
||||||
|
label: "We don't recognize that address. Do you want to create a new account? [Y/n] "
|
||||||
|
, type: 'text' // TODO boolean with default Y or N
|
||||||
|
}).then(function (result) {
|
||||||
|
if (!result.input) {
|
||||||
|
result.input = 'Y';
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO needs backup address if email is on same domain as login
|
||||||
|
result.input = result.input.toLowerCase();
|
||||||
|
|
||||||
|
if ('y' !== result.input) {
|
||||||
|
return getCurrentUserEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sameProvider) {
|
||||||
|
return { node: email, type: 'email' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return form.ask({
|
||||||
|
label: "What's your recovery email (or cloud mail) address? ", type: 'email'
|
||||||
|
}).then(function (recoveryResult) {
|
||||||
|
return {
|
||||||
|
node: email
|
||||||
|
, type: 'name'
|
||||||
|
, recovery: recoveryResult.result || recoveryResult.input
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmCreateAccount();
|
||||||
|
}).then(function (user) {
|
||||||
|
// TODO skip if token exists locally
|
||||||
|
var email = (user.recovery || user.node);
|
||||||
|
form.println("Sending login code to '" + email + "'...");
|
||||||
|
return OAUTH3.authn.otp(oauth3._providerDirectives, { email: email }).then(function (otpResult) {
|
||||||
|
return form.ask({
|
||||||
|
label: "What's your login code? "
|
||||||
|
, help: "(it was sent to '" + email + "' and looks like 1234-5678-9012)"
|
||||||
|
// onkeyup
|
||||||
|
// ondebounce
|
||||||
|
// onchange
|
||||||
|
// regexp // html5 name?
|
||||||
|
, onReturnAsync: function (rs, ws, input/*, ch*/) {
|
||||||
|
var formatted = input.toLowerCase().replace(/[^\d]+/g, '');
|
||||||
|
|
||||||
|
if (12 !== formatted.length) {
|
||||||
|
return form.PromiseA.reject(new Error("invalid code please try again in the format xxxx-yyyy-zzzz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted = formatted.match(/.{4,4}/g).join('-');
|
||||||
|
|
||||||
|
if (14 !== formatted.split('').length) {
|
||||||
|
return form.PromiseA.reject(new Error("invalid code '" + formatted + "', please try again xxxx-yyyy-zzzz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
username: email
|
||||||
|
, username_type: 'email'
|
||||||
|
, client_id: OAUTH3.uri.normalize(oauth3._providerDirectives.issuer)
|
||||||
|
, client_uri: OAUTH3.uri.normalize(oauth3._providerDirectives.issuer)
|
||||||
|
, otp_code: formatted
|
||||||
|
, otp_uuid: otpResult.data.uuid
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns session instead of input
|
||||||
|
var colors = require('colors');
|
||||||
|
form.setStatus(colors.dim("authenticating with server..."));
|
||||||
|
return OAUTH3.authn.resourceOwnerPassword(oauth3._providerDirectives, data).then(function (result) {
|
||||||
|
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."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentUserEmail() {
|
||||||
|
return form.ask({
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
};
|
12
bower.json
12
bower.json
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "oauth3",
|
"name": "oauth3.js",
|
||||||
"authors": [
|
"authors": [
|
||||||
"AJ ONeal <aj@daplie.com>"
|
"AJ ONeal <aj@daplie.com>"
|
||||||
],
|
],
|
||||||
@ -26,12 +26,18 @@
|
|||||||
"sign"
|
"sign"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://git.daplie.com/OAuth3/oauth3.js",
|
"homepage": "https://git.oauth3.org/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"
|
||||||
}
|
}
|
||||||
|
97
browserify/crypto.fallback.js
Normal file
97
browserify/crypto.fallback.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var createHash = require('create-hash');
|
||||||
|
var pbkdf2 = require('pbkdf2');
|
||||||
|
var aes = require('browserify-aes');
|
||||||
|
var ec = require('elliptic/lib/elliptic/ec')('p256');
|
||||||
|
|
||||||
|
function sha256(buf) {
|
||||||
|
return createHash('sha256').update(buf).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPbkdf2(password, salt) {
|
||||||
|
// Derived AES key is 128 bit, and the function takes a size in bytes.
|
||||||
|
return pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256');
|
||||||
|
}
|
||||||
|
|
||||||
|
function encrypt(key, iv, data) {
|
||||||
|
var cipher = aes.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
|
||||||
|
|
||||||
|
return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(key, iv, data) {
|
||||||
|
var decipher = aes.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
|
||||||
|
|
||||||
|
decipher.setAuthTag(Buffer(data.slice(-16)));
|
||||||
|
return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bnToBuffer(bn, size) {
|
||||||
|
var buf = bn.toArrayLike(Buffer);
|
||||||
|
|
||||||
|
if (!size || buf.length === size) {
|
||||||
|
return buf;
|
||||||
|
} else if (buf.length < size) {
|
||||||
|
return Buffer.concat([Buffer(size-buf.length).fill(0), buf]);
|
||||||
|
} else if (buf.length > size) {
|
||||||
|
throw new Error('EC signature number bigger than expected');
|
||||||
|
}
|
||||||
|
throw new Error('invalid size "'+size+'" converting BigNumber to Buffer');
|
||||||
|
}
|
||||||
|
function bnToB64(bn) {
|
||||||
|
var b64 = bnToBuffer(bn).toString('base64');
|
||||||
|
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '');
|
||||||
|
}
|
||||||
|
function genEcdsaKeyPair() {
|
||||||
|
var key = ec.genKeyPair();
|
||||||
|
var pubJwk = {
|
||||||
|
key_ops: ['verify']
|
||||||
|
, kty: 'EC'
|
||||||
|
, crv: 'P-256'
|
||||||
|
, x: bnToB64(key.getPublic().getX())
|
||||||
|
, y: bnToB64(key.getPublic().getY())
|
||||||
|
};
|
||||||
|
|
||||||
|
var privJwk = JSON.parse(JSON.stringify(pubJwk));
|
||||||
|
privJwk.key_ops = ['sign'];
|
||||||
|
privJwk.d = bnToB64(key.getPrivate());
|
||||||
|
|
||||||
|
return {privateKey: privJwk, publicKey: pubJwk};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sign(jwk, msg) {
|
||||||
|
var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64'));
|
||||||
|
var sig = key.sign(sha256(msg));
|
||||||
|
return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify(jwk, msg, signature) {
|
||||||
|
var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')});
|
||||||
|
var sig = {
|
||||||
|
r: Buffer(signature.slice(0, signature.length/2))
|
||||||
|
, s: Buffer(signature.slice(signature.length/2))
|
||||||
|
};
|
||||||
|
return key.verify(sha256(msg), sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promiseWrap(func) {
|
||||||
|
return function() {
|
||||||
|
var args = arguments;
|
||||||
|
// This fallback file should only be used when the browser doesn't support everything we
|
||||||
|
// need with WebCrypto. Since it is only used in the browser we should be able to assume
|
||||||
|
// that OAUTH3 has been placed in the global scope and that we can access it here.
|
||||||
|
return new OAUTH3.PromiseA(function (resolve) {
|
||||||
|
resolve(func.apply(null, args));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
exports.sha256 = promiseWrap(sha256);
|
||||||
|
exports.pbkdf2 = promiseWrap(runPbkdf2);
|
||||||
|
exports.encrypt = promiseWrap(encrypt);
|
||||||
|
exports.decrypt = promiseWrap(decrypt);
|
||||||
|
exports.sign = promiseWrap(sign);
|
||||||
|
exports.verify = promiseWrap(verify);
|
||||||
|
exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair);
|
||||||
|
}());
|
25
gulpfile.js
Normal file
25
gulpfile.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var browserify = require('browserify');
|
||||||
|
var source = require('vinyl-source-stream');
|
||||||
|
var streamify = require('gulp-streamify');
|
||||||
|
var uglify = require('gulp-uglify');
|
||||||
|
var rename = require('gulp-rename');
|
||||||
|
|
||||||
|
gulp.task('default', function () {
|
||||||
|
return browserify('./browserify/crypto.fallback.js', {standalone: 'OAUTH3_crypto_fallback'}).bundle()
|
||||||
|
.pipe(source('browserify/crypto.fallback.js'))
|
||||||
|
.pipe(rename('oauth3.crypto.fallback.js'))
|
||||||
|
.pipe(gulp.dest('./'))
|
||||||
|
.pipe(streamify(uglify()))
|
||||||
|
.pipe(rename('oauth3.crypto.fallback.min.js'))
|
||||||
|
.pipe(gulp.dest('./'))
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('watch', function () {
|
||||||
|
gulp.watch('browserify/*.js', [ 'default' ]);
|
||||||
|
});
|
||||||
|
}());
|
96
navigator.auth.js
Normal file
96
navigator.auth.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
86
oauth3.account.js
Normal file
86
oauth3.account.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
;(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));
|
859
oauth3.core.js
859
oauth3.core.js
File diff suppressed because it is too large
Load Diff
16581
oauth3.crypto.fallback.js
Normal file
16581
oauth3.crypto.fallback.js
Normal file
File diff suppressed because it is too large
Load Diff
8
oauth3.crypto.fallback.min.js
vendored
Normal file
8
oauth3.crypto.fallback.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
295
oauth3.crypto.js
Normal file
295
oauth3.crypto.js
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
;(function (exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
|
||||||
|
|
||||||
|
OAUTH3.crypto = {};
|
||||||
|
try {
|
||||||
|
OAUTH3.crypto.core = require('./oauth3.node.crypto');
|
||||||
|
} catch (error) {
|
||||||
|
OAUTH3.crypto.core = {};
|
||||||
|
OAUTH3.crypto.core.ready = false;
|
||||||
|
var finishBeforeReady = [];
|
||||||
|
var deferedCalls = [];
|
||||||
|
|
||||||
|
// We don't currently have a fallback method for this function, so we assign
|
||||||
|
// it directly to the core object instead of the webCrypto object.
|
||||||
|
OAUTH3.crypto.core.randomBytes = function (size) {
|
||||||
|
var buf = OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(size));
|
||||||
|
return OAUTH3.PromiseA.resolve(buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
var webCrypto = {};
|
||||||
|
|
||||||
|
var deferCryptoCall = function(name) {
|
||||||
|
return function() {
|
||||||
|
var args = arguments;
|
||||||
|
return new OAUTH3.PromiseA(function(resolve, reject) {
|
||||||
|
deferedCalls.push(function(){
|
||||||
|
try {
|
||||||
|
webCrypto[name].apply(webCrypto, args)
|
||||||
|
.then(function(result){
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.sha256 = deferCryptoCall("sha256");
|
||||||
|
webCrypto.sha256 = function (buf) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.pbkdf2 = deferCryptoCall("pbkdf2");
|
||||||
|
webCrypto.pbkdf2 = function (password, salt) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey'])
|
||||||
|
.then(function (key) {
|
||||||
|
var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}};
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']);
|
||||||
|
})
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.encrypt = deferCryptoCall("encrypt");
|
||||||
|
webCrypto.encrypt = function (rawKey, iv, data) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.decrypt = deferCryptoCall("decrypt");
|
||||||
|
webCrypto.decrypt = function (rawKey, iv, data) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.genEcdsaKeyPair = deferCryptoCall("genEcdsaKeyPair");
|
||||||
|
webCrypto.genEcdsaKeyPair = function () {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
|
||||||
|
.then(function (keyPair) {
|
||||||
|
return OAUTH3.PromiseA.all([
|
||||||
|
OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.privateKey)
|
||||||
|
, OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.publicKey)
|
||||||
|
]);
|
||||||
|
}).then(function (jwkPair) {
|
||||||
|
return { privateKey: jwkPair[0], publicKey: jwkPair[1] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.sign = deferCryptoCall("sign");
|
||||||
|
webCrypto.sign = function (jwk, msg) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, msg);
|
||||||
|
})
|
||||||
|
.then(function (sig) {
|
||||||
|
return new Uint8Array(sig);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.core.verify = deferCryptoCall("verify");
|
||||||
|
webCrypto.verify = function (jwk, msg, signature) {
|
||||||
|
// If the JWK has properties that should only exist on the private key or is missing
|
||||||
|
// "verify" in the key_ops, importing in as a public key won't work.
|
||||||
|
if (jwk.hasOwnProperty('d') || jwk.hasOwnProperty('key_ops')) {
|
||||||
|
jwk = JSON.parse(JSON.stringify(jwk));
|
||||||
|
delete jwk.d;
|
||||||
|
delete jwk.key_ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkWebCrypto() {
|
||||||
|
/* global OAUTH3_crypto_fallback */
|
||||||
|
var loadFallback = function() {
|
||||||
|
var prom;
|
||||||
|
loadFallback = function () { return prom; };
|
||||||
|
|
||||||
|
prom = new OAUTH3.PromiseA(function (resolve) {
|
||||||
|
var body = document.getElementsByTagName('body')[0];
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onreadystatechange = function () {
|
||||||
|
if (this.readyState === 'complete' || this.readyState === 'loaded') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js';
|
||||||
|
body.appendChild(script);
|
||||||
|
});
|
||||||
|
return prom;
|
||||||
|
};
|
||||||
|
function checkException(name, func) {
|
||||||
|
return OAUTH3.PromiseA.resolve().then(func)
|
||||||
|
.then(function () {
|
||||||
|
OAUTH3.crypto.core[name] = webCrypto[name];
|
||||||
|
}, function (err) {
|
||||||
|
console.warn('error with WebCrypto', name, '- using fallback', err);
|
||||||
|
return loadFallback().then(function () {
|
||||||
|
OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function checkResult(name, expected, func) {
|
||||||
|
|
||||||
|
finishBeforeReady.push(checkException(name, function () {
|
||||||
|
return func()
|
||||||
|
.then(function (result) {
|
||||||
|
if (typeof expected === typeof result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return OAUTH3._base64.bufferToUrlSafe(result);
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
if (result !== 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 dataBuf = OAUTH3._base64.urlSafeToBuffer('1234567890abcdefghijklmn');
|
||||||
|
var keyBuf = OAUTH3._base64.urlSafeToBuffer('l_Aeoqk6ePjwjCYrlHrgrg');
|
||||||
|
var encBuf = OAUTH3._base64.urlSafeToBuffer('Ji_gEtcNElUONSR4Mf9S75davXjh_6-oQN9AgO5UF8rERw');
|
||||||
|
checkResult('sha256', 'BwMveUm2V1axuERvUoxM4dScgNl9yKhER9a6p80GXj4', function () {
|
||||||
|
return webCrypto.sha256(dataBuf);
|
||||||
|
});
|
||||||
|
checkResult('pbkdf2', OAUTH3._base64.bufferToUrlSafe(keyBuf), function () {
|
||||||
|
return webCrypto.pbkdf2('password', zeroBuf);
|
||||||
|
});
|
||||||
|
checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () {
|
||||||
|
return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf);
|
||||||
|
});
|
||||||
|
checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () {
|
||||||
|
return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf);
|
||||||
|
});
|
||||||
|
|
||||||
|
var jwk = {
|
||||||
|
kty: "EC"
|
||||||
|
, crv: "P-256"
|
||||||
|
, d: "ChXx7ea5YtEltCufA8CVb0lQv3glcCfcSpEgdedgIP0"
|
||||||
|
, x: "Akt5ZDbytcKS5UQMURvGb_UIMS4qFctDwrX8bX22ato"
|
||||||
|
, y: "cV7nhpWNT1FeRIbdold4jLtgsEpZBFcNy3p2E5mqvto"
|
||||||
|
};
|
||||||
|
var sig = OAUTH3._base64.urlSafeToBuffer('nc3F8qeP8OXpfqPD9tTcFQg0Wfp37RTAppLPIKE1ZupR_8Aba64hNExwd1dOk802OFQxaECPDZCkKe7WA9RXAg');
|
||||||
|
checkResult('verify', true, function() {
|
||||||
|
return webCrypto.verify(jwk, dataBuf, sig);
|
||||||
|
});
|
||||||
|
// The results of these functions are less predictable, so we can't check their return value.
|
||||||
|
finishBeforeReady.push(checkException('genEcdsaKeyPair', function () {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
checkWebCrypto();
|
||||||
|
}
|
||||||
|
|
||||||
|
OAUTH3.crypto.thumbprintJwk = function (jwk) {
|
||||||
|
var keys;
|
||||||
|
if (jwk.kty === 'EC') {
|
||||||
|
keys = ['crv', 'x', 'y'];
|
||||||
|
} else if (jwk.kty === 'RSA') {
|
||||||
|
keys = ['e', 'n'];
|
||||||
|
} else if (jwk.kty === 'oct') {
|
||||||
|
keys = ['k'];
|
||||||
|
} else {
|
||||||
|
return OAUTH3.PromiseA.reject(new Error('invalid JWK key type ' + jwk.kty));
|
||||||
|
}
|
||||||
|
keys.push('kty');
|
||||||
|
keys.sort();
|
||||||
|
|
||||||
|
var missing = keys.filter(function (name) { return !jwk.hasOwnProperty(name); });
|
||||||
|
if (missing.length > 0) {
|
||||||
|
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
|
||||||
|
// 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))
|
||||||
|
.then(OAUTH3._base64.bufferToUrlSafe);
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.createKeyPair = function () {
|
||||||
|
// TODO: maybe support other types of key pairs, not just ECDSA P-256
|
||||||
|
return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) {
|
||||||
|
return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
|
||||||
|
keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
|
||||||
|
keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
|
||||||
|
return keyPair;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.encryptKeyPair = function (keyPair, password) {
|
||||||
|
var saltProm = OAUTH3.crypto.core.randomBytes(16);
|
||||||
|
var kekProm = saltProm.then(function (salt) {
|
||||||
|
return OAUTH3.crypto.core.pbkdf2(password, salt);
|
||||||
|
});
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.all([
|
||||||
|
kekProm
|
||||||
|
, saltProm
|
||||||
|
, OAUTH3.crypto.core.randomBytes(12)
|
||||||
|
, ]).then(function (results) {
|
||||||
|
var kek = results[0];
|
||||||
|
var salt = results[1];
|
||||||
|
var ecdsaIv = results[2];
|
||||||
|
|
||||||
|
var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey));
|
||||||
|
return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) {
|
||||||
|
return {
|
||||||
|
publicKey: keyPair.publicKey
|
||||||
|
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted)
|
||||||
|
, salt: OAUTH3._base64.bufferToUrlSafe(salt)
|
||||||
|
, ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
|
||||||
|
, };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto.decryptKeyPair = function (storedObj, password) {
|
||||||
|
var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt);
|
||||||
|
var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
|
||||||
|
var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);
|
||||||
|
|
||||||
|
return OAUTH3.crypto.core.pbkdf2(password, salt)
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
|
||||||
|
})
|
||||||
|
.then(OAUTH3._binStr.bufferToBinStr)
|
||||||
|
.then(JSON.parse)
|
||||||
|
.then(function (privateKey) {
|
||||||
|
return {
|
||||||
|
privateKey: privateKey
|
||||||
|
, publicKey: storedObj.publicKey
|
||||||
|
, };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}('undefined' !== typeof exports ? exports : window));
|
104
oauth3.dns.js
104
oauth3.dns.js
@ -31,15 +31,23 @@ 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
|
+ '/api/com.daplie.domains/accounts/' + session.token.sub + '/devices/'
|
||||||
//+ '/devices/' + device + '/'
|
+ device + '/' + tld + '/' + sld + '/' + (sub || '')
|
||||||
+ '/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;
|
||||||
});
|
});
|
||||||
@ -47,28 +55,15 @@ 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 + '/'
|
+ '/devices/' + device + '/' + tld + '/' + sld + '/' + (sub || '')
|
||||||
+ '/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) {
|
||||||
@ -76,4 +71,69 @@ 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));
|
||||||
|
@ -3,6 +3,35 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@ -16,4 +45,102 @@ 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));
|
||||||
|
804
oauth3.issuer.js
804
oauth3.issuer.js
@ -3,39 +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.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
|
||||||
@ -58,8 +25,16 @@ 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;
|
||||||
|
|
||||||
var clientUrl = OAUTH3.url.normalize(client.url);
|
// it doesn't matter who the referrer is as long as the destination
|
||||||
|
// is an authorized destination for the client in question
|
||||||
|
// (though it may not hurt to pass the referrer's info on to the client)
|
||||||
|
var clientUrl = OAUTH3.url.normalize(client);
|
||||||
var redirectUrl = OAUTH3.url.normalize(query.redirect_uri);
|
var redirectUrl = OAUTH3.url.normalize(query.redirect_uri);
|
||||||
|
|
||||||
// General rule:
|
// General rule:
|
||||||
@ -72,6 +47,18 @@ 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) {
|
||||||
@ -110,13 +97,11 @@ 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/org.oauth3.provider/access_token
|
// POST https://example.com/api/issuer@oauth3.org/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) {
|
||||||
@ -125,15 +110,13 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var scope = opts.scope || directive.authn_scope;
|
var args = directive.access_token;
|
||||||
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
|
||||||
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: grantType
|
, grant_type: 'password'
|
||||||
, 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
|
||||||
@ -148,23 +131,21 @@ 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 (clientUri) {
|
if (opts.client_uri) {
|
||||||
params.clientAgreeTos = clientAgreeTos;
|
params.clientAgreeTos = 'oauth3.org/tos/draft'; // opts.clientAgreeTos || opts.client_agree_tos;
|
||||||
if (!clientAgreeTos) {
|
if (!params.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 {
|
||||||
@ -172,7 +153,7 @@ OAUTH3.urls.resourceOwnerPassword = function (directive, opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: OAUTH3.url.resolve(directive.issuer, uri)
|
url: OAUTH3.url.resolve(directive.api, uri)
|
||||||
, method: args.method
|
, method: args.method
|
||||||
, data: body
|
, data: body
|
||||||
};
|
};
|
||||||
@ -180,6 +161,10 @@ 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.");
|
||||||
@ -194,35 +179,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 must supply options.method as either 'GET', or 'POST'");
|
console.warn("You should 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) {
|
||||||
console.warn("You should supply options.scope as a space-delimited string of scopes");
|
throw new Error("You must supply options.scope as a comma-delimited string of scopes");
|
||||||
}
|
}
|
||||||
if (-1 === ['token', 'code'].indexOf(opts.response_type)) {
|
if ('string' !== typeof opts.sub) {
|
||||||
throw new Error("You must supply options.response_type as 'token' or 'code'");
|
console.log("provide 'sub' to urls.grants to specify the PPID for the client");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = OAUTH3.url.resolve(directive.issuer, directive.grants.url)
|
var url = OAUTH3.url.resolve(directive.api, grantsDir.url)
|
||||||
.replace(/(:azp|:client_id)/g, OAUTH3.uri.normalize(opts.client_id || opts.client_uri))
|
.replace(/(:sub|:account_id)/g, opts.session.token.sub || 'ISSUER:GRANT:TOKEN_SUB:UNDEFINED')
|
||||||
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
|
.replace(/(:azp|:client_id)/g, !opts.all && OAUTH3.uri.normalize(opts.client_id || opts.client_uri) || '')
|
||||||
|
.replace(/\/\/$/, '/') // if there's a double slash due to the sub not existing
|
||||||
;
|
;
|
||||||
var data = {
|
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
|
||||||
, tenant_id: opts.tenant_id
|
, sub: opts.sub
|
||||||
};
|
};
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,43 +218,119 @@ 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");
|
||||||
|
}
|
||||||
|
|
||||||
OAUTH3.authn = {};
|
if (!opts) {
|
||||||
|
throw new Error("You must supply a directive and an options object.");
|
||||||
|
}
|
||||||
|
if (!(opts.azp || opts.client_id)) {
|
||||||
|
throw new Error("You must supply options.client_id.");
|
||||||
|
}
|
||||||
|
if (!opts.session) {
|
||||||
|
throw new Error("You must supply options.session.");
|
||||||
|
}
|
||||||
|
if (!opts.method) {
|
||||||
|
opts.method = tokenDir.method || 'POST';
|
||||||
|
}
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
grant_type: 'issuer_token'
|
||||||
|
, client_id: opts.azp || opts.client_id
|
||||||
|
, azp: opts.azp || opts.client_id
|
||||||
|
, aud: opts.aud
|
||||||
|
, exp: opts.exp
|
||||||
|
, refresh_token: opts.refresh_token
|
||||||
|
, refresh_exp: opts.refresh_exp
|
||||||
|
};
|
||||||
|
|
||||||
|
var url = OAUTH3.url.resolve(directive.api, tokenDir.url);
|
||||||
|
var body;
|
||||||
|
if ('GET' === opts.method) {
|
||||||
|
url += '?' + OAUTH3.query.stringify(params);
|
||||||
|
} else {
|
||||||
|
body = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: opts.method
|
||||||
|
, url: url
|
||||||
|
, data: body
|
||||||
|
, session: opts.session
|
||||||
|
};
|
||||||
|
};
|
||||||
|
OAUTH3.urls.publishKey = function (directive, opts) {
|
||||||
|
var jwkDir = directive.publish_jwk;
|
||||||
|
if (!jwkDir) {
|
||||||
|
throw new Error("provider doesn't support publishing public keys");
|
||||||
|
}
|
||||||
|
if (!opts) {
|
||||||
|
throw new Error("You must supply a directive and an options object.");
|
||||||
|
}
|
||||||
|
if (!opts.session) {
|
||||||
|
throw new Error("You must supply 'options.session'.");
|
||||||
|
}
|
||||||
|
if (!(opts.public_key || opts.publicKey)) {
|
||||||
|
throw new Error("You must supply 'options.public_key'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = OAUTH3.url.resolve(directive.api, jwkDir.url)
|
||||||
|
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
|
||||||
|
;
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: jwkDir.method || opts.method || 'POST'
|
||||||
|
, url: url
|
||||||
|
, data: opts.public_key || opts.publicKey
|
||||||
|
, session: opts.session
|
||||||
|
};
|
||||||
|
};
|
||||||
|
OAUTH3.urls.credentialMeta = function (directive, opts) {
|
||||||
|
return OAUTH3.url.resolve(directive.api, directive.credential_meta.url)
|
||||||
|
.replace(':type', 'email')
|
||||||
|
.replace(':id', opts.email)
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.authn = OAUTH3.authn || {};
|
||||||
OAUTH3.authn.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
|
||||||
, url: OAUTH3.url.resolve(directive.issuer, directive.credential_meta.url)
|
// TODO client_uri
|
||||||
.replace(':type', 'email')
|
, url: url
|
||||||
.replace(':id', opts.email)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
OAUTH3.authn.otp = function (directive, opts) {
|
OAUTH3.urls.otp = function (directive, opts) {
|
||||||
var preq = {
|
// TODO client_uri
|
||||||
|
return {
|
||||||
method: directive.credential_otp.method || 'POST'
|
method: directive.credential_otp.method || 'POST'
|
||||||
, url: OAUTH3.url.resolve(directive.issuer, directive.credential_otp.url)
|
, url: OAUTH3.url.resolve(directive.api, 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'
|
||||||
, client_id: directive.issuer // In this case, the issuer is its own client
|
// TODO unbreak the client_uri option (if broken)
|
||||||
, client_uri: directive.issuer
|
, client_id: /*opts.client_id ||*/ OAUTH3.uri.normalize(directive.issuer) // In this case, the issuer is its own client
|
||||||
|
, client_uri: /*opts.client_uri ||*/ OAUTH3.uri.normalize(directive.issuer)
|
||||||
, request_otp: true
|
, request_otp: true
|
||||||
, 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;
|
||||||
|
|
||||||
//var scope = opts.scope;
|
return OAUTH3.request(OAUTH3.urls.resourceOwnerPassword(directive, opts)).then(function (resp) {
|
||||||
//var appId = opts.appId;
|
var data = resp.data;
|
||||||
return OAUTH3.discover(providerUri, opts).then(function (directive) {
|
|
||||||
var prequest = OAUTH3.urls.resourceOwnerPassword(directive, opts);
|
|
||||||
|
|
||||||
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));
|
||||||
@ -279,20 +340,52 @@ 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 || '';
|
var scope = clientParams.scope || 'authn@oauth3.org';
|
||||||
var clientObj = clientParams;
|
if ('authn@oauth3.org' === scope.toString()) {
|
||||||
|
// implicit ppid grant is automatic
|
||||||
|
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
|
||||||
|
// TODO check user preference if implicit ppid grant is allowed
|
||||||
|
//return generateToken(session, clientObj);
|
||||||
|
}
|
||||||
|
|
||||||
if (!scope) {
|
return OAUTH3.hooks.grants.get(session.token.sub, clientUri).then(function (granted) {
|
||||||
scope = 'oauth3_authn';
|
if (granted) {
|
||||||
|
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, {
|
||||||
@ -300,74 +393,31 @@ 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 (grantResults) {
|
}).then(function (results) {
|
||||||
var grants;
|
return results.grants;
|
||||||
var grantedScopes;
|
}, function (err) {
|
||||||
var grantedScopesMap;
|
if (!/no .*grants .*found/i.test(err.message)) {
|
||||||
var pendingScopes;
|
throw err;
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
location.href = callbackUrl;
|
return [];
|
||||||
return;
|
});
|
||||||
}
|
}).then(function (granted) {
|
||||||
|
var requested = OAUTH3.scope.parse(scope);
|
||||||
if ('oauth3_authn' === scope) {
|
var accepted = [];
|
||||||
// implicit ppid grant is automatic
|
var pending = [];
|
||||||
console.warn('[security] fix scope checking on backend so that we can do automatic grants');
|
requested.forEach(function (scp) {
|
||||||
// TODO check user preference if implicit ppid grant is allowed
|
if (granted.indexOf(scp) < 0) {
|
||||||
//return generateToken(session, clientObj);
|
pending.push(scp);
|
||||||
}
|
} 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 {
|
||||||
pending: pendingScopes // not yet accepted
|
requested: requested // all requested, now
|
||||||
, granted: grantedScopes // all granted, ever
|
, granted: granted // all granted, ever
|
||||||
, requested: scopes // all requested, now
|
, accepted: accepted // intersection of granted (ever) and requested (now)
|
||||||
, accepted: acceptedScopes // granted (ever) and requested (now)
|
, pending: pending // not yet accepted
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -376,144 +426,406 @@ 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);
|
||||||
return OAUTH3.request(OAUTH3.urls.grants(directive, opts), opts).then(function (grantsResult) {
|
}).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(opts.client_id + '-client', grants.client);
|
OAUTH3.hooks.grants.set(grants.sub, grants.azp, grants);
|
||||||
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: OAUTH3.hooks.grants.get(opts.client_id + '-client')
|
client: grants.azp
|
||||||
, grants: OAUTH3.hooks.grants.get(opts.client_id) || []
|
, clientSub: grants.azpSub
|
||||||
|
, 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);
|
||||||
|
}
|
||||||
|
|
||||||
scopes.new = scopes.new || [];
|
var prom;
|
||||||
|
if (scopes.new) {
|
||||||
if ('token' === clientParams.response_type) {
|
prom = OAUTH3.authz.grants(providerUri, {
|
||||||
// get token and redirect client-side
|
session: session
|
||||||
return OAUTH3.authz.grants(providerUri, {
|
, method: 'POST'
|
||||||
method: 'POST'
|
|
||||||
, client_id: clientParams.client_uri
|
, client_id: clientParams.client_uri
|
||||||
, client_uri: clientParams.client_uri
|
|
||||||
, scope: scopes.granted.concat(scopes.new).join(',')
|
|
||||||
, response_type: clientParams.response_type
|
|
||||||
, referrer: clientParams.referrer
|
, referrer: clientParams.referrer
|
||||||
, session: session
|
, scope: scopes.accepted.concat(scopes.new).join(',')
|
||||||
, debug: clientParams.debug
|
});
|
||||||
}).then(function (results) {
|
} else {
|
||||||
|
prom = OAUTH3.PromiseA.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
OAUTH3.url.redirect(clientParams, scopes, results);
|
return prom.then(function () {
|
||||||
|
return OAUTH3.hooks.keyPairs.get(session.token.sub);
|
||||||
|
}).then(function (keyPair) {
|
||||||
|
if (!keyPair) {
|
||||||
|
return OAUTH3.discover(providerUri, {
|
||||||
|
client_id: providerUri
|
||||||
|
, debug: clientParams.debug
|
||||||
|
}).then(function (directive) {
|
||||||
|
return OAUTH3.request(OAUTH3.urls.clientToken(directive, {
|
||||||
|
method: 'POST'
|
||||||
|
, session: session
|
||||||
|
, referrer: clientParams.referrer
|
||||||
|
, response_type: clientParams.response_type
|
||||||
|
, client_id: clientParams.client_uri
|
||||||
|
, azp: clientParams.client_uri
|
||||||
|
, aud: clientParams.aud
|
||||||
|
, exp: clientParams.exp
|
||||||
|
, refresh_token: clientParams.refresh_token
|
||||||
|
, refresh_exp: clientParams.refresh_exp
|
||||||
|
, debug: clientParams.debug
|
||||||
|
})).then(function (result) {
|
||||||
|
return result.originalData || result.data;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if ('code' === clientParams.response_type) {
|
|
||||||
// get token and redirect server-side
|
return OAUTH3.hooks.grants.get(keyPair.sub, clientParams.client_uri).then(function (grant) {
|
||||||
// (requires insecure form post as per spec)
|
var now = Math.floor(Date.now()/1000);
|
||||||
//OAUTH3.requests.authorizationDecision();
|
var payload = {
|
||||||
window.alert("Authorization Code Redirect NOT IMPLEMENTED");
|
iat: now
|
||||||
throw new Error("Authorization Code Redirect NOT IMPLEMENTED");
|
, iss: providerUri
|
||||||
}
|
, aud: clientParams.aud || providerUri
|
||||||
|
, azp: clientParams.client_uri
|
||||||
|
, sub: grant.azpSub
|
||||||
|
, scope: OAUTH3.scope.stringify(grant.scope)
|
||||||
|
, };
|
||||||
|
|
||||||
|
var signProms = [];
|
||||||
|
signProms.push(OAUTH3.jwt.sign(Object.assign({
|
||||||
|
exp: calcExpiration(clientParams.exp || '1h', now)
|
||||||
|
}, payload), keyPair));
|
||||||
|
// if (clientParams.refresh_token) {
|
||||||
|
signProms.push(OAUTH3.jwt.sign(Object.assign({
|
||||||
|
exp: calcExpiration(clientParams.refresh_exp, now)
|
||||||
|
}, payload), keyPair));
|
||||||
|
// }
|
||||||
|
return OAUTH3.PromiseA.all(signProms).then(function (tokens) {
|
||||||
|
console.log('created new tokens for client');
|
||||||
|
return {
|
||||||
|
access_token: tokens[0]
|
||||||
|
, refresh_token: tokens[1]
|
||||||
|
, scope: OAUTH3.scope.stringify(grant.scope)
|
||||||
|
, token_type: 'bearer'
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(function (session) {
|
||||||
|
// TODO limit refresh token to an expirable token
|
||||||
|
// TODO inform client not to persist token
|
||||||
|
OAUTH3.url.redirect(clientParams, scopes, session);
|
||||||
|
}, function (err) {
|
||||||
|
console.error('unexpected error creating client tokens', err);
|
||||||
|
OAUTH3.url.redirect(clientParams, scopes, {error: err});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
OAUTH3.requests = {};
|
OAUTH3.requests = {};
|
||||||
|
//OAUTH3.accounts = {};
|
||||||
OAUTH3.requests.accounts = {};
|
OAUTH3.requests.accounts = {};
|
||||||
OAUTH3.requests.accounts.update = function (directive, session, opts) {
|
OAUTH3.urls.accounts = {};
|
||||||
var dir = directive.update_account || {
|
OAUTH3.urls.accounts._ = function (directives, directive, session, opts) {
|
||||||
method: 'POST'
|
opts = opts || {};
|
||||||
, url: 'https://' + directive.provider_url + '/api/org.oauth3.provider/accounts/:accountId'
|
var dir = directive || {
|
||||||
|
//url: OAUTH3.url.normalize(directives.api) + '/api/issuer@oauth3.org/accounts/:accountId'
|
||||||
|
url: OAUTH3.url.normalize(directives.api) + '/api/issuer@oauth3.org/acl/profiles/:accountId'
|
||||||
|
//, method: 'GET'
|
||||||
, bearer: 'Bearer'
|
, bearer: 'Bearer'
|
||||||
};
|
};
|
||||||
var url = dir.url
|
var url = dir.url
|
||||||
.replace(/:accountId/, opts.accountId)
|
.replace(/:accountId/, opts.accountId || '')
|
||||||
|
.replace(/\/$/, '')
|
||||||
;
|
;
|
||||||
|
|
||||||
return OAUTH3.request({
|
return {
|
||||||
method: dir.method || 'POST'
|
url: url
|
||||||
, url: url
|
//, method: dir.method || 'POST'
|
||||||
|
, session: session
|
||||||
|
/*
|
||||||
, headers: {
|
, headers: {
|
||||||
'Authorization': (dir.bearer || 'Bearer') + ' ' + session.accessToken
|
'Authorization': (dir.bearer || 'Bearer') + ' ' + (session.access_token || 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
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
OAUTH3.requests.accounts.create = function (directive, session, account) {
|
return urlObj;
|
||||||
var dir = directive.create_account || {
|
|
||||||
method: 'POST'
|
|
||||||
, url: 'https://' + directive.issuer + '/api/org.oauth3.provider/accounts'
|
|
||||||
, bearer: 'Bearer'
|
|
||||||
};
|
};
|
||||||
var data = {
|
OAUTH3.urls.accounts.create = function (directives, session, account) {
|
||||||
|
var urlObj = OAUTH3.urls.accounts._(directives, directives.create_account, session);
|
||||||
|
var profile = {
|
||||||
|
nick: account.display_name
|
||||||
|
// "name" is unique and what would be reserved in a url {{name}}.issuer.org or issuer.org/users/{{name}}
|
||||||
|
, name: account.name
|
||||||
|
, comment: account.comment
|
||||||
|
, display_name: account.display_name
|
||||||
|
, priority: account.priority
|
||||||
|
};
|
||||||
|
var credentials = [ { token: session.access_token } ];
|
||||||
|
urlObj.method = (directives.create_account || { method: 'POST' }).method;
|
||||||
|
urlObj.json = {
|
||||||
// TODO fix the server to just use one scheme
|
// TODO fix the server to just use one scheme
|
||||||
// account = { nick, self: { comment, username } }
|
// account = { nick, self: { comment, username } }
|
||||||
// account = { name, comment, display_name, priority }
|
// account = { name, comment, display_name, priority }
|
||||||
account: {
|
credentials: credentials
|
||||||
nick: account.display_name
|
, profile: profile
|
||||||
, name: account.name
|
// 'account' is deprecated in favor of 'profile'
|
||||||
, comment: account.comment
|
, account: profile
|
||||||
, display_name: account.display_name
|
// 'logins' is deprecated in favor of 'credentials'
|
||||||
, priority: account.priority
|
, logins: credentials
|
||||||
, self: {
|
};
|
||||||
nick: account.display_name
|
return urlObj;
|
||||||
, name: account.name
|
};
|
||||||
, comment: account.comment
|
OAUTH3.requests.accounts.get = function (directives, session) {
|
||||||
, display_name: account.display_name
|
var urlObj = OAUTH3.urls.accounts.get(directives, session);
|
||||||
, priority: account.priority
|
return OAUTH3.request(urlObj);
|
||||||
}
|
};
|
||||||
}
|
OAUTH3.requests.accounts.update = function (directives, session, opts) {
|
||||||
, logins: [
|
var urlObj = OAUTH3.urls.accounts.update(directives, session, opts);
|
||||||
{
|
return OAUTH3.request(urlObj);
|
||||||
token: session.access_token
|
};
|
||||||
}
|
OAUTH3.requests.accounts.create = function (directive, session, account) {
|
||||||
]
|
var urlObj = OAUTH3.urls.accounts.create(directives, session, account);
|
||||||
|
return OAUTH3.request(urlObj);
|
||||||
};
|
};
|
||||||
|
|
||||||
return OAUTH3.request({
|
OAUTH3.hooks.grants = {
|
||||||
method: dir.method || 'POST'
|
get: function (id, clientUri) {
|
||||||
, url: dir.url
|
OAUTH3.hooks._checkStorage('grants', 'get');
|
||||||
, session: session
|
|
||||||
, data: data
|
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.get(id, OAUTH3.uri.normalize(clientUri)));
|
||||||
|
}
|
||||||
|
, set: function (id, clientUri, grants) {
|
||||||
|
OAUTH3.hooks._checkStorage('grants', 'set');
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("id is not set");
|
||||||
|
}
|
||||||
|
if (!clientUri) {
|
||||||
|
throw new Error("clientUri is not set");
|
||||||
|
}
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.set(id, OAUTH3.uri.normalize(clientUri), grants));
|
||||||
|
}
|
||||||
|
, all: function () {
|
||||||
|
OAUTH3.hooks._checkStorage('grants', 'all');
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.all());
|
||||||
|
}
|
||||||
|
, clear: function () {
|
||||||
|
OAUTH3.hooks._checkStorage('grants', 'clear');
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.grants.clear());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
OAUTH3.hooks.keyPairs = {
|
||||||
|
get: function (id) {
|
||||||
|
OAUTH3.hooks._checkStorage('keyPairs', 'get');
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("id is not set");
|
||||||
|
}
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.get(id));
|
||||||
|
}
|
||||||
|
, set: function (id, keyPair) {
|
||||||
|
OAUTH3.hooks._checkStorage('keyPairs', 'set');
|
||||||
|
|
||||||
|
if (!keyPair && id.privateKey && id.publicKey && id.sub) {
|
||||||
|
keyPair = id;
|
||||||
|
id = keyPair.sub;
|
||||||
|
}
|
||||||
|
if (!keyPair) {
|
||||||
|
return OAUTH3.PromiseA.reject(new Error("no key pair provided to save"));
|
||||||
|
}
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("id is not set");
|
||||||
|
}
|
||||||
|
keyPair.sub = keyPair.sub || id;
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.set(id, keyPair));
|
||||||
|
}
|
||||||
|
, all: function () {
|
||||||
|
OAUTH3.hooks._checkStorage('keyPairs', 'all');
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.all());
|
||||||
|
}
|
||||||
|
, clear: function () {
|
||||||
|
OAUTH3.hooks._checkStorage('keyPairs', 'clear');
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.resolve(OAUTH3._hooks.keyPairs.clear());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
OAUTH3.hooks.session.get = function (providerUri, id) {
|
||||||
|
OAUTH3.hooks._checkStorage('sessions', 'get');
|
||||||
|
var sessProm = OAUTH3.PromiseA.resolve(OAUTH3._hooks.sessions.get(providerUri, id));
|
||||||
|
if (providerUri !== OAUTH3.clientUri(window.location)) {
|
||||||
|
return sessProm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessProm.then(function (session) {
|
||||||
|
if (session && OAUTH3.jwt.freshness(session.token) === 'fresh') {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3.hooks.keyPairs.all().then(function (keyPairs) {
|
||||||
|
var pair;
|
||||||
|
if (id) {
|
||||||
|
pair = keyPairs[id];
|
||||||
|
} else if (Object.keys(keyPairs).length === 1) {
|
||||||
|
id = Object.keys(keyPairs)[0];
|
||||||
|
pair = keyPairs[id];
|
||||||
|
} else if (Object.keys(keyPairs).length > 1) {
|
||||||
|
console.error("too many users, don't know which key to use");
|
||||||
|
}
|
||||||
|
if (!pair) {
|
||||||
|
// even if the access token isn't fresh, the session might have a refresh token
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Math.floor(Date.now()/1000);
|
||||||
|
var payload = {
|
||||||
|
iat: now
|
||||||
|
, iss: providerUri
|
||||||
|
, aud: providerUri
|
||||||
|
, azp: providerUri
|
||||||
|
, sub: pair.sub || id
|
||||||
|
, scope: ''
|
||||||
|
, exp: now + 3600
|
||||||
|
};
|
||||||
|
return OAUTH3.jwt.sign(payload, pair.privateKey).then(function (token) {
|
||||||
|
console.log('created new token for provider');
|
||||||
|
return OAUTH3.hooks.session.refresh(
|
||||||
|
{ provider_uri: providerUri, client_uri: providerUri || providerUri }
|
||||||
|
, { access_token: token }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
OAUTH3.hooks.grants = {
|
|
||||||
// Provider Only
|
OAUTH3._defaultStorage.grants = {
|
||||||
set: function (clientUri, newGrants) {
|
prefix: 'grants-'
|
||||||
clientUri = OAUTH3.uri.normalize(clientUri);
|
, get: function (id, clientUri) {
|
||||||
console.warn('[oauth3.hooks.setGrants] PLEASE IMPLEMENT -- Your Fault');
|
var key = this.prefix + id+'/'+clientUri;
|
||||||
console.warn(newGrants);
|
var result = JSON.parse(window.localStorage.getItem(key) || 'null');
|
||||||
if (!this._cache) { this._cache = {}; }
|
return OAUTH3.PromiseA.resolve(result);
|
||||||
console.log('clientUri, newGrants');
|
|
||||||
console.log(clientUri, newGrants);
|
|
||||||
this._cache[clientUri] = newGrants;
|
|
||||||
return newGrants;
|
|
||||||
}
|
}
|
||||||
, get: function (clientUri) {
|
, set: function (id, clientUri, grants) {
|
||||||
clientUri = OAUTH3.uri.normalize(clientUri);
|
var key = this.prefix + id+'/'+clientUri;
|
||||||
console.warn('[oauth3.hooks.getGrants] PLEASE IMPLEMENT -- Your Fault');
|
window.localStorage.setItem(key, JSON.stringify(grants));
|
||||||
if (!this._cache) { this._cache = {}; }
|
return this.get(clientUri);
|
||||||
console.log('clientUri, existingGrants');
|
}
|
||||||
console.log(clientUri, this._cache[clientUri]);
|
, all: function () {
|
||||||
return this._cache[clientUri];
|
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,30 +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._base64.btoa = function (b64) {
|
|
||||||
// http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome
|
|
||||||
return (exports.btoa || require('btoa'))(b64);
|
|
||||||
};
|
|
||||||
OAUTH3._base64.encodeUrlSafe = function (b64) {
|
|
||||||
// Base64 to URL-safe Base64
|
|
||||||
b64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
|
|
||||||
b64 = b64.replace(/=+/g, '');
|
|
||||||
return OAUTH3._base64.btoa(b64);
|
|
||||||
};
|
|
||||||
|
|
||||||
OAUTH3.jwt.encode = function (parts) {
|
|
||||||
parts.header = parts.header || { alg: 'none', typ: 'jwt' };
|
|
||||||
parts.signature = parts.signature || '';
|
|
||||||
|
|
||||||
var result = [
|
|
||||||
OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.header, null))
|
|
||||||
, OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.payload, null))
|
|
||||||
, parts.signature // should already be url-safe base64
|
|
||||||
].join('.');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
||||||
var providerUri = directive.issuer;
|
var providerUri = directive.issuer;
|
||||||
|
|
||||||
@ -50,12 +26,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
OAUTH3.authz.scopes = function () {
|
OAUTH3.authz.scopes = function () {
|
||||||
return {
|
return OAUTH3.PromiseA.resolve({
|
||||||
pending: ['oauth3_authn'] // not yet accepted
|
pending: [ 'authn@oauth3.org' ] // not yet accepted
|
||||||
, granted: [] // all granted, ever
|
, granted: [] // all granted, ever
|
||||||
, requested: ['oauth3_authn'] // all requested, now
|
, requested: [ 'authn@oauth3.org' ] // all requested, now
|
||||||
, accepted: [] // granted (ever) and requested (now)
|
, accepted: [] // granted (ever) and requested (now)
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
OAUTH3.authz.grants = function (providerUri, opts) {
|
OAUTH3.authz.grants = function (providerUri, opts) {
|
||||||
if ('POST' === opts.method) {
|
if ('POST' === opts.method) {
|
||||||
@ -82,12 +58,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
OAUTH3._mockToken = function (providerUri, opts) {
|
OAUTH3._mockToken = function (providerUri, opts) {
|
||||||
var accessToken = OAUTH3.jwt.encode({
|
var payload = { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope };
|
||||||
header: { alg: 'none' }
|
return OAUTH3.crypto._signPayload(payload).then(function (accessToken) {
|
||||||
, payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope }
|
|
||||||
, signature: "fakeSig"
|
|
||||||
});
|
|
||||||
|
|
||||||
return OAUTH3.hooks.session.refresh(
|
return OAUTH3.hooks.session.refresh(
|
||||||
opts.session || {
|
opts.session || {
|
||||||
provider_uri: providerUri
|
provider_uri: providerUri
|
||||||
@ -100,6 +72,7 @@
|
|||||||
, scope: opts.scope
|
, scope: opts.scope
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
}('undefined' !== typeof exports ? exports : window));
|
}('undefined' !== typeof exports ? exports : window));
|
||||||
|
22
oauth3.ng.js
22
oauth3.ng.js
@ -1,13 +1,12 @@
|
|||||||
;(function () {
|
;(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular
|
var modules = {
|
||||||
.module('org.oauth3', [])
|
azp: [
|
||||||
.service('Oauth3', [
|
|
||||||
'$timeout'
|
'$timeout'
|
||||||
, '$q'
|
, '$q'
|
||||||
, function Oauth3($timeout, $q) {
|
, '$rootScope'
|
||||||
|
, 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+
|
||||||
@ -30,9 +29,20 @@ angular
|
|||||||
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)
|
||||||
|
;
|
||||||
}());
|
}());
|
||||||
|
106
oauth3.node.crypto.js
Normal file
106
oauth3.node.crypto.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var OAUTH3 = require('./oauth3.core.js').OAUTH3;
|
||||||
|
var ec = require('elliptic').ec('p256');
|
||||||
|
|
||||||
|
function randomBytes(size) {
|
||||||
|
return new OAUTH3.PromiseA(function (resolve, reject) {
|
||||||
|
crypto.randomBytes(size, function (err, buf) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(buf);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(buf) {
|
||||||
|
return crypto.createHash('sha256').update(buf).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pbkdf2(password, salt) {
|
||||||
|
// Derived AES key is 128 bit, and the function takes a size in bytes.
|
||||||
|
return crypto.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256');
|
||||||
|
}
|
||||||
|
|
||||||
|
function encrypt(key, iv, data) {
|
||||||
|
var cipher = crypto.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
|
||||||
|
|
||||||
|
return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrypt(key, iv, data) {
|
||||||
|
var decipher = crypto.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv));
|
||||||
|
|
||||||
|
decipher.setAuthTag(Buffer(data.slice(-16)));
|
||||||
|
return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bnToBuffer(bn, size) {
|
||||||
|
var buf = bn.toArrayLike(Buffer);
|
||||||
|
|
||||||
|
if (!size || buf.length === size) {
|
||||||
|
return buf;
|
||||||
|
} else if (buf.length < size) {
|
||||||
|
return Buffer.concat([Buffer(size-buf.length).fill(0), buf]);
|
||||||
|
} else if (buf.length > size) {
|
||||||
|
throw new Error('EC signature number bigger than expected');
|
||||||
|
}
|
||||||
|
throw new Error('invalid size "'+size+'" converting BigNumber to Buffer');
|
||||||
|
}
|
||||||
|
function bnToB64(bn) {
|
||||||
|
var b64 = bnToBuffer(bn).toString('base64');
|
||||||
|
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '');
|
||||||
|
}
|
||||||
|
function genEcdsaKeyPair() {
|
||||||
|
var key = ec.genKeyPair();
|
||||||
|
var pubJwk = {
|
||||||
|
key_ops: ['verify']
|
||||||
|
, kty: 'EC'
|
||||||
|
, crv: 'P-256'
|
||||||
|
, x: bnToB64(key.getPublic().getX())
|
||||||
|
, y: bnToB64(key.getPublic().getY())
|
||||||
|
};
|
||||||
|
|
||||||
|
var privJwk = JSON.parse(JSON.stringify(pubJwk));
|
||||||
|
privJwk.key_ops = ['sign'];
|
||||||
|
privJwk.d = bnToB64(key.getPrivate());
|
||||||
|
|
||||||
|
return {privateKey: privJwk, publicKey: pubJwk};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sign(jwk, msg) {
|
||||||
|
var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64'));
|
||||||
|
var sig = key.sign(sha256(msg));
|
||||||
|
return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify(jwk, msg, signature) {
|
||||||
|
var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')});
|
||||||
|
var sig = {
|
||||||
|
r: Buffer(signature.slice(0, signature.length/2))
|
||||||
|
, s: Buffer(signature.slice(signature.length/2))
|
||||||
|
};
|
||||||
|
return key.verify(sha256(msg), sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function promiseWrap(func) {
|
||||||
|
return function() {
|
||||||
|
var args = arguments;
|
||||||
|
return new OAUTH3.PromiseA(function (resolve) {
|
||||||
|
resolve(func.apply(null, args));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
exports.sha256 = promiseWrap(sha256);
|
||||||
|
exports.pbkdf2 = promiseWrap(pbkdf2);
|
||||||
|
exports.encrypt = promiseWrap(encrypt);
|
||||||
|
exports.decrypt = promiseWrap(decrypt);
|
||||||
|
exports.sign = promiseWrap(sign);
|
||||||
|
exports.verify = promiseWrap(verify);
|
||||||
|
exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair);
|
||||||
|
exports.randomBytes = randomBytes;
|
||||||
|
}());
|
101
oauth3.node.js
Normal file
101
oauth3.node.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
//var OAUTH3 = require('./oauth3.core.js').OAUTH3;
|
||||||
|
var OAUTH3 = require('./oauth3.issuer.js').OAUTH3;
|
||||||
|
// used for OAUTH3.urls.resourcePasswordOwner
|
||||||
|
// used for OAUTH3.authn.loginMeta
|
||||||
|
// used for OAUTH3.authn.otp
|
||||||
|
// used for OAUTH3.authn.resourcePasswordOwner
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
var requestAsync = PromiseA.promisify(require('request'));
|
||||||
|
var crypto = require('crypto');
|
||||||
|
|
||||||
|
OAUTH3.PromiseA = PromiseA;
|
||||||
|
OAUTH3._discoverHelper = function(providerUri, opts) {
|
||||||
|
return OAUTH3._node.discover(providerUri, opts);
|
||||||
|
};
|
||||||
|
OAUTH3._requestHelper = function (preq, opts) {
|
||||||
|
/*
|
||||||
|
if (opts && opts.directives) {
|
||||||
|
preq.url = OAUTH3.url.resolve(opts.directives.issuer, preq.url);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
return OAUTH3._node.request(preq, opts);
|
||||||
|
};
|
||||||
|
OAUTH3._base64.atob = function (base64) {
|
||||||
|
return new Buffer(base64, 'base64').toString('utf8');
|
||||||
|
};
|
||||||
|
OAUTH3._base64.btoa = function (text) {
|
||||||
|
return new Buffer(text, 'utf8').toString('base64');
|
||||||
|
};
|
||||||
|
OAUTH3._defaultStorage = require('./oauth3.node.storage');
|
||||||
|
|
||||||
|
OAUTH3._node = {};
|
||||||
|
OAUTH3._node.discover = function(providerUri/*, opts*/) {
|
||||||
|
return OAUTH3.request({
|
||||||
|
method: 'GET'
|
||||||
|
, url: OAUTH3.url.normalize(providerUri) + '/.well-known/oauth3/directives.json'
|
||||||
|
}).then(function (resp) {
|
||||||
|
return resp.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
OAUTH3._node.request = function(preq/*, _sys*/) {
|
||||||
|
var data = {
|
||||||
|
method: preq.method
|
||||||
|
, url: preq.url || preq.uri
|
||||||
|
, headers: preq.headers
|
||||||
|
, timeout: preq.timeout || undefined
|
||||||
|
, json: preq.data || preq.body || preq.json || undefined // TODO which to use?
|
||||||
|
, formData: preq.formData || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
//console.log('DEBUG request');
|
||||||
|
//console.log(preq.url || preq.uri);
|
||||||
|
//console.log(data.json);
|
||||||
|
|
||||||
|
return requestAsync(data).then(OAUTH3._node._parseJson);
|
||||||
|
};
|
||||||
|
OAUTH3._node._parseJson = function (resp) {
|
||||||
|
var err;
|
||||||
|
var json = resp.body;
|
||||||
|
|
||||||
|
// TODO toCamelCase
|
||||||
|
if (!(resp.statusCode >= 200 && resp.statusCode < 400)) {
|
||||||
|
err = new Error("bad response code: " + resp.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('resp.body', typeof resp.body);
|
||||||
|
if ('string' === typeof json) {
|
||||||
|
try {
|
||||||
|
json = JSON.parse(json);
|
||||||
|
} catch(e) {
|
||||||
|
err = err || (new Error('response not parsable: ' + resp.body));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle both Oauth2- and node-style errors
|
||||||
|
if (json && json.error) {
|
||||||
|
err = new Error(json.error.message || json.error_description || JSON.stringify(json.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
err.result = json;
|
||||||
|
return PromiseA.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: resp.headers
|
||||||
|
, status: resp.statusCode
|
||||||
|
, data: json
|
||||||
|
};
|
||||||
|
};
|
||||||
|
OAUTH3._logoutHelper = function(/*directives, opts*/) {
|
||||||
|
// TODO allow prompting of which account
|
||||||
|
return OAUTH3.PromiseA.reject(new Error("logout not yet implemented for node.js"));
|
||||||
|
};
|
||||||
|
OAUTH3._node.randomState = function () {
|
||||||
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
};
|
||||||
|
OAUTH3.randomState = OAUTH3._node.randomState;
|
||||||
|
|
||||||
|
module.exports = OAUTH3;
|
137
oauth3.node.storage.js
Normal file
137
oauth3.node.storage.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
var fs = PromiseA.promisifyAll(require('fs'));
|
||||||
|
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 = {
|
||||||
|
directives: {
|
||||||
|
all: function () {
|
||||||
|
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
|
||||||
|
try {
|
||||||
|
return require(path.join(directivesdir, providerUri + '.json'));
|
||||||
|
} catch(e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, set: function (providerUri, directives) {
|
||||||
|
return fs.writeFileAsync(
|
||||||
|
path.join(directivesdir, providerUri + '.json')
|
||||||
|
, JSON.stringify(directives, null, 2)
|
||||||
|
).then(function () {
|
||||||
|
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: {
|
||||||
|
all: function (providerUri) {
|
||||||
|
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
|
||||||
|
try {
|
||||||
|
return PromiseA.resolve(require(path.join(metadir, key + '.json')));
|
||||||
|
} catch(e) {
|
||||||
|
return PromiseA.resolve(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, set: function (key, value) {
|
||||||
|
return fs.writeFileAsync(path.join(metadir, key + '.json'), JSON.stringify(value, null, 2)).then(function () {
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -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/org.oauth3.tunnel/accounts/' + session.token.sub + '/token'
|
+ '/api/tunnel@oauth3.org/accounts/' + session.token.sub + '/token'
|
||||||
, session: session
|
, session: session
|
||||||
, data: {
|
, data: {
|
||||||
domains: opts.data.domains
|
domains: opts.data.domains
|
||||||
|
25
package.json
25
package.json
@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "oauth3",
|
"name": "oauth3.js",
|
||||||
"version": "1.0.0",
|
"version": "1.2.2",
|
||||||
"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",
|
||||||
"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.daplie.com:OAuth3/oauth3.js.git"
|
"url": "git@git.oauth3.org:OAuth3/oauth3.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"oauth",
|
"oauth",
|
||||||
@ -30,6 +31,24 @@
|
|||||||
"log",
|
"log",
|
||||||
"sign"
|
"sign"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": "^3.5.0",
|
||||||
|
"elliptic": "^6.4.0",
|
||||||
|
"request": "^2.81.0",
|
||||||
|
"terminal-forms.js": "git+https://git.oauth3.org/OAuth3/terminal-forms.js.git#v1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"browserify-aes": "^1.0.6",
|
||||||
|
"create-hash": "^1.1.2",
|
||||||
|
"pbkdf2": "^3.0.9",
|
||||||
|
"browserify": "^14.1.0",
|
||||||
|
"gulp": "^3.9.1",
|
||||||
|
"gulp-cli": "^1.2.2",
|
||||||
|
"gulp-rename": "^1.2.2",
|
||||||
|
"gulp-streamify": "^1.0.2",
|
||||||
|
"gulp-uglify": "^2.1.0",
|
||||||
|
"vinyl-source-stream": "^1.1.0"
|
||||||
|
},
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "(MIT OR Apache-2.0)"
|
"license": "(MIT OR Apache-2.0)"
|
||||||
}
|
}
|
||||||
|
@ -1,560 +0,0 @@
|
|||||||
;(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));
|
|
@ -1,42 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
@ -1,473 +0,0 @@
|
|||||||
;(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));
|
|
@ -1,302 +0,0 @@
|
|||||||
;(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));
|
|
@ -1,109 +0,0 @@
|
|||||||
(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);
|
|
||||||
}());
|
|
@ -1,445 +0,0 @@
|
|||||||
/* 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));
|
|
@ -1,158 +0,0 @@
|
|||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,97 +0,0 @@
|
|||||||
;(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));
|
|
@ -1,24 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
1
well-known
Symbolic link
1
well-known
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
_apis
|
@ -1,12 +0,0 @@
|
|||||||
{ "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/" }
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
<!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>
|
|
Loading…
x
Reference in New Issue
Block a user