Merge branch 'v1' of git.daplie.com:Daplie/oauth3.js into v1

This commit is contained in:
AJ ONeal 2017-02-13 12:57:03 -07:00
commit 4657fcdb12
15 changed files with 672 additions and 30 deletions

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Redirecting...</title>
<style>
body {
background-color: #ffcccc;
}
</style>
</head>
<body>
Redirecting...
<!-- TODO permanently cache with appcache (or service worker?) -->
<!-- TODO slim this all down to a single file -->
<script src="/assets/org.oauth3/oauth3.core.js"></script>
<script src="callback.js"></script>
</body>
</html>

View File

@ -0,0 +1,66 @@
(function () {
"use strict";
var loc = window.location;
var loginWinObj = window.OAUTH3_CORE.queryparse(loc.hash || loc.search);
var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/callback.html]";
if (loginWinObj.debug) {
console.warn(prefix, "DEBUG MODE ENABLED. Automatic redirects disabled.");
}
// '--oauth3-callback-' prefix exist for security so that an attacker can't social engineer execution an arbitrary function
// TODO finalize name of '--oauth3-callback-', this will be a defacto standard
// TODO maybe call it 'self-xss-' or 'hack-my-account-' to discourage people from doing dumb things?
var callbackName = '--oauth3-callback-' + loginWinObj.state;
console.log(prefix, loc.href);
console.log('Parsed URL Obj: ', loginWinObj);
console.log('callbackName: ', callbackName);
window.oauth3complete = function () {
// The hacks that used to be necessary for this on iOS should no longer be necessary in iOS 9+
// see https://bugs.chromium.org/p/chromium/issues/detail?id=136610 and https://crbug.com/423444
// TODO Should we still create an abstraction for older versions?
if (window.parent) {
// iframe
try {
window.parent[callbackName](loginWinObj);
return;
} catch(e) {
console.warn(e);
}
}
if (window.opener) {
try {
window.opener[callbackName](loginWinObj);
return;
} catch(e) {
console.warn(e);
}
}
console.error("neither window.parent nor window.opener existed to complete callback");
/*
// the caller should close (or signal to close) the window
try {
window.close();
} catch (err) {
console.log('Error: ', err);
}
*/
};
if (!loginWinObj.debug) {
window.oauth3complete();
}
else {
document.body.innerHTML = window.location.hostname + window.location.pathname
+ '<br/><br/>You\'ve passed the \'debug\' parameter so we\'re pausing'
+ ' to let you look at logs or whatever it is that you intended to do.'
+ '<br/><br/>Continue with callback: <a href="javascript:window.oauth3complete()">javascript:window.oauth3complete()</' + 'a>';
return;
}
}());

View File

View File

View File

@ -0,0 +1,330 @@
(function () {
'use strict';
console.log('[DAPLIE oauth3 directives.js]');
console.log(window.location);
var iter = 0;
function main() {
var rpc = {};
//var myself = location.protocol + '//' + location.host + location.pathname;
var incoming;
var forwarding = {};
var err;
var browserState;
var browserCallback;
var action;
function parseParams() {
var params = {};
function parseParamsString(str) {
str.substr(1).split('&').filter(function (el) { return el; }).forEach(function (pair) {
pair = pair.split('=');
var key = decodeURIComponent(pair[0]);
var val = decodeURIComponent(pair[1]);
if (params[key]) {
console.warn("overwriting key '" + key + "' '" + params[key] + "'");
}
params[key] = val;
});
}
parseParamsString(window.location.search);
// handle cases where hash is treated like it's own href
// TODO /#/?search=blah
parseParamsString(window.location.hash);
return params;
}
function querystringify(params) {
var arr = [];
Object.keys(params).forEach(function (k) {
arr.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));
});
return arr.join('&');
}
function phoneAway(/*redirectURi, params*/) {
// TODO test for ? / #
window.location.href = incoming.redirect_uri + '#' + querystringify(forwarding);
}
function lintAndSetRedirectable(browserState, params) {
if (!params.redirect_uri) {
window.alert('redirect_uri not defined');
err = new Error('redirect_uri not defined');
console.error(err.message);
console.warn(err.stack);
params.redirect_uri = document.referer;
return false;
}
if (!browserState) {
forwarding.error = "E_NO_BROWSER_STATE";
forwarding.error_description = "you must specify a state parameter";
return false;
}
localStorage.setItem('oauth3.states.' + browserState, JSON.stringify(params));
return true;
}
function redirectCallback() {
var redirect_uri = incoming.redirect_uri;
forwarding.callback = browserState;
forwarding.action = 'close';
var url = redirect_uri + '#' + querystringify(forwarding);
console.log('[debug] redirect_uri + params:', url);
window.location.href = url;
setTimeout(function () {
if (iter >= 3) {
console.log("dancing way too much... stopping now");
return;
}
iter += 1;
console.log("I'm dancing by myse-e-elf");
// in case I'm redirecting to myself
main();
}, 0);
}
rpc = {};
// Act as a provider and log the user out
rpc.logout = function (browserState, incoming) {
var url;
if (!lintAndSetRedirectable(browserState, incoming)) {
// TODO fail
}
localStorage.setItem('oauth3.states.' + browserState, JSON.stringify(incoming));
url = '/#/logout/' + browserState;
// TODO specify specific account or all?
window.location.href = url;
setTimeout(function () {
// in case I'm redirecting to myself
main();
}, 0);
};
// Act as a provider and inform the consumer the logout is complete
rpc.logout_callback = function (browserState/*, incoming*/) {
// TODO pass redirect_uri and state through here so we can avoid localStorage
var forwarding = {};
var originalRequest;
if (!browserState) {
forwarding.error = "E_NO_BROWSER_STATE";
forwarding.error_description = "you must specify a state parameter";
if (incoming.redirect_uri) {
phoneAway(incoming.redirect_uri, forwarding);
}
return;
}
originalRequest = JSON.parse(localStorage.getItem('oauth3.states.' + browserState));
forwarding.action = 'close';
forwarding.state = browserState;
//phoneAway(originalRequest.redirect_uri, forwarding);
window.location.href = originalRequest.redirect_uri + '#' + querystringify(forwarding);
};
rpc.directives = function (browserState, incoming) {
if (!lintAndSetRedirectable(browserState, incoming)) {
phoneAway();
return;
}
var updatedAt = new Date(localStorage.getItem('oauth3.directives.updated_at')).valueOf();
var fresh = (Date.now() - updatedAt) < (24 * 60 * 60 * 1000);
var directives = localStorage.getItem('oauth3.directives');
var redirected = false;
function redirectIf() {
if (redirected) {
return;
}
redirected = true;
redirectCallback();
}
if (directives) {
forwarding.directives = directives;
redirectIf();
if (fresh) {
return;
}
}
var req = new XMLHttpRequest();
req.open('GET', '.well-known/oauth3.json', true);
req.addEventListener('readystatechange', function () {
if (4 !== req.readyState) {
return;
}
if (200 !== req.status) {
forwarding.error = "E_STATUS_" + req.status;
forwarding.error_description = "expected 200 OK json or text response for oauth3.json but got '" + req.status + "'";
redirectIf();
return;
}
try {
directives = btoa(JSON.stringify(JSON.parse(req.responseText)));
forwarding.directives = directives;
forwarding.callback = browserState;
localStorage.setItem('oauth3.directives', directives);
localStorage.setItem('oauth3.directives.updated_at', new Date().toISOString());
} catch(e) {
forwarding.error = "E_PARSE_JSON";
forwarding.error_description = e.message;
console.error(forwarding.error);
console.error(forwarding.error_description);
console.error(req.responseText);
}
redirectIf();
});
req.send();
};
// the provider is contacting me
rpc.close = function (browserState, incoming) {
incoming.callback = browserState;
catchAll();
};
// the provider is contacting me
rpc.redirect = function (/*browserState, incoming*/) {
catchAll();
};
function catchAll() {
function phoneHome() {
if (browserCallback === 'completeLogin') {
// Deprecated
console.log('[deprecated] callback completeLogin');
(window.opener||window.parent).completeLogin(null, null, incoming);
} else {
console.log('[DEBUG] I would be closed by my parent now');
console.log('__oauth3_' + browserCallback);
console.log(window.opener && window.opener['__oauth3_' + browserCallback]);
console.log(window.parent && window.parent['__oauth3_' + browserCallback]);
console.log(incoming);
(window.opener||window.parent)['__oauth3_' + browserCallback](incoming);
}
}
if (!(incoming.browser_state || incoming.state)) {
window.alert("callback URLs should include 'browser_state' (authorization code)"
+ " or 'state' (implicit grant))");
}
setTimeout(function () {
// opener is for popup window, new tab
// parent is for iframe
phoneHome();
}, 10);
// iOS Webview (namely Chrome) workaround
setTimeout(function () {
console.log('I would close now');
// XXX OAUTH3 DEBUG FRAME XXX // make this easy to find
window.open('', '_self', '');
window.close();
}, 50);
setTimeout(function () {
var i;
var len = localStorage.length;
var key;
var json;
var fresh;
for (i = 0; i < len; i += 1) {
key = localStorage.key(i);
// TODO check updatedAt
if (/^oauth3\./.test(key)) {
try {
json = localStorage.getItem(key);
if (json) {
json = JSON.parse(json);
}
} catch (e) {
// ignore
json = null;
}
fresh = json && (Date.now() - json.updatedAt < (5 * 60 * 1000));
if (!fresh) {
localStorage.removeItem(key);
}
}
}
forwarding.updatedAt = Date.now();
localStorage.setItem('oauth3.' + (forwarding.browser_state || forwarding.state), JSON.stringify(forwarding));
}, 0);
}
function parseAction(params) {
if (params.action) {
return params.action;
}
if (params.close) {
return 'close';
}
if (params.logout_callback) {
return 'logout_callback';
}
if (params.logout) {
return 'logout';
}
if (params.callback) {
return 'close';
}
if (params.directives) {
return 'directives';
}
return 'redirect';
}
incoming = parseParams();
browserState = incoming.browser_state || incoming.state;
action = parseAction(incoming);
forwarding.url = window.location.href;
forwarding.browser_state = browserState;
forwarding.state = browserState;
if (!incoming.provider_uri) {
browserCallback = incoming.callback || browserState;
} else {
// deprecated
browserCallback = 'completeLogin';
}
console.log('[debug]', action, incoming);
if (rpc[action]) {
rpc[action](browserState, incoming);
} else {
window.alert('unsupported action');
}
}
main();
}());

View File

@ -0,0 +1,9 @@
{ "authorization_dialog": { "url": "#/authorization_dialog" }
, "access_token": { "method": "POST", "url": "api/org.oauth3.provider/access_token" }
, "otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" }
, "credential_otp": { "method": "POST" , "url": "api/org.oauth3.provider/otp" }
, "credential_meta": { "url": "api/org.oauth3.provider/logins/meta/:type/:id" }
, "credential_create": { "method": "POST" , "url": "api/org.oauth3.provider/logins" }
, "grants": { "method": "GET", "url": "api/org.oauth3.provider/grants/:azp/:sub" }
, "authorization_decision": { "method": "POST", "url": "api/org.oauth3.provider/authorization_decision" }
}

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
background-color: #ffcccc;
}
</style>
</head>
<body>
OAuth3 RPC
<script src="/assets/com.jquery/jquery-3.1.1.js"></script>
<script src="/assets/org.oauth3/oauth3.core.js"></script>
<script>
'use strict';
// TODO what about search within hash?
var prefix = "(" + window.location.hostname + ") [.well-known/oauth3/]";
var params = OAUTH3_CORE.queryparse(window.location.hash || window.location.search);
if (params.debug) {
console.warn(prefix, "DEBUG MODE ENABLED. Automatic redirects disabled.");
}
console.log(prefix, 'hash||search:');
console.log(window.location.hash || window.location.search);
console.log(prefix, 'params:');
console.log(params);
$.ajax({ url: 'directives.json' }).then(function (resp) {
var b64 = btoa(JSON.stringify(resp, null, 0))
var urlsafe64 = OAUTH3_CORE.utils.base64ToUrlSafeBase64(b64);
var redirect;
console.log(prefix, 'directives');
console.log(resp);
console.log(prefix, 'base64');
console.log(urlsafe64);
// TODO try postMessage back to redirect_uri domain right here
// window.postMessage();
// TODO make sure it's https NOT http
// NOTE: this can be only up to 2,083 characters
console.log(prefix, 'params.redirect_uri:', params.redirect_uri);
redirect = params.redirect_uri + '?' + OAUTH3_CORE.querystringify({
state: params.state
, directives: urlsafe64
, debug: params.debug || undefined
})
console.log(prefix, 'redirect');
console.log(redirect);
if (!params.debug) {
window.location = redirect;
} else {
// yes, we're violating the security lint with purpose
document.body.innerHTML += window.location.host + window.location.pathname
+ '<br/><br/>You\'ve passed the \'debug\' parameter so we\'re pausing'
+ ' to let you look at logs or whatever it is that you intended to do.'
+ '<br/><br/>Continue with redirect: <a href="' + redirect + '">' + redirect + '</' + 'a>';
}
});
</script>
</body>
</html>

View File

View File

101
README.md
View File

@ -1,6 +1,88 @@
oauth3.js
=========
The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation
(Yes! works in browsers and node.js with no extra dependencies or bloat and no hacks!)
Instead of bloating your webapp and ruining the mobile experience,
you can use a single, small javascript file for all OAuth3 providers
(and almost all OAuth2 providers) with a seemless experience.
Also, instead of complicated (or worse - insecure) CLI and Desktop login methods,
you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit)
with 0 pain.
Installation
------------
**Easy Install** for Web Apps (including Mobile):
1. In your web site / web app folder create a folder called `assets`
2. Inside of `assets` create another folder called `org.oauth3`
3. Download [oauth.js-v1.zip](https://git.daplie.com/Daplie/oauth3.js/repository/archive.zip?ref=v1)
4. Double-click to unzip the folder.
5. Copy `oauth3.js` and `oauth3.browser.js` to `assets/org.oauth3`
**Advanced Installation with `git`**
```
# Navigate to your web site or web app
pushd /path/to/your/web/app
# clone the project as assets/org.oauth3
mkdir -p assets
git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
pushd assests/org.oauth3
git checkout v1
popd
# symlink `.well-known/oauth3` to `assets/org.oauth3/.well-known/oauth3`
mkdir -p .well-known
ln -sf ../assets/org.oauth3/.well-known/oauth3 .well-known/oauth3
```
**Advanced Installation with `bower`**
```
# Install to bower_components
bower install oauth3
# create a `.well-known` folder and an `assets` folder
mkdir -p .well-known assets
# symlink `.well-known/oauth3` to `bower_components/oauth3/.well-known/oauth3`
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
# symlink `assets/org.oauth3` to `bower_components/oauth3`
ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3
ln -sf ../bower_components/oauth3 assets/org.oauth3
```
Usage
-----
Update your HTML to include the the following script tags:
```
<script src="assets/org.oauth3/oauth3.js"></script>
<script src="assets/org.oauth3/oauth3.browser.js"></script>
```
If you use jQuery you should also include
```
<script src="assets/org.oauth3/oauth3.jquery.js"></script>
```
Stable API
----------
Public utilities for browser and node.js:
* `querystringify(query)`
@ -14,6 +96,25 @@ URL generation:
* `loginCode`
* `resourceOwnerPassword`
Roadmap
-------
* v1.0 - "implicit grant" authorization with examples
* popup
* iframe
* documentation
* v1.1 - cleanup
* in-flow discovery
* smallest possible size
* inline windowing (non-promisable callback)
* async set/get
* logout
* v1.2 - features
* "authorization code" flow
* "broker" flow
* v1.3 - features
* remove grants
URI vs URL
----------

View File

@ -13,7 +13,8 @@
}
var browser = exports.OAUTH3_BROWSER = {
clientUri: function (location) {
window: window
, clientUri: function (location) {
return OAUTH3_CORE.normalizeUri(location.host + location.pathname);
}
, discover: function (providerUri, opts) {
@ -118,18 +119,36 @@
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 ('background' === opts.type) {
if (!opts.windowType) {
opts.windowType = 'popup';
}
if ('background' === opts.windowType) {
promise = browser.insertIframe(url, state, opts);
} else if ('popup' === opts.type) {
} 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 not specified or not type yet implemented");
throw new Error("login framing method options.windowType not specified or not type yet implemented");
}
return promise.then(function (params) {
@ -245,7 +264,7 @@
//
// Logins
//
, requests: {
, authn: {
authorizationRedirect: function (providerUri, opts) {
// TODO get own directives
@ -260,9 +279,19 @@
}
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(
@ -276,6 +305,15 @@
}
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) {
@ -455,14 +493,14 @@
, code: data.code
, access_token: data.accessToken
, expires_at: data.expiresAt
, expires_in: data.expiresIn
, access_token: data.access_token
, expires_at: data.expires_at
, expires_in: data.expires_in
, scope: data.scope
, refresh_token: data.refreshToken
, refresh_expires_at: data.refreshExpiresAt
, refresh_expires_in: data.refreshExpiresIn
, 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) {
@ -507,6 +545,7 @@
}, 50);
}
};
browser.requests = browser.authn;
Object.keys(browser).forEach(function (key) {
if ('requests' === key) {

View File

@ -175,10 +175,10 @@
, signature: parts[2] // should remain url-safe base64
};
}
, getFreshness: function (meta, staletime, now) {
, getFreshness: function (tokenMeta, staletime, now) {
staletime = staletime || (15 * 60);
now = now || Date.now();
var fresh = ((parseInt(meta.exp, 10) || 0) - Math.round(now / 1000));
var fresh = ((parseInt(tokenMeta.exp, 10) || 0) - Math.round(now / 1000));
if (fresh >= staletime) {
return 'fresh';

View File

@ -158,7 +158,7 @@
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.meta.sub)
.replace(/(:sub|:account_id)/g, opts.session.token.sub)
;
var data = {
client_id: opts.client_id
@ -206,12 +206,14 @@
//$('.js-user-avatar').attr('src', userAvatar);
/*
console.log('grants options');
console.log(loc.hash);
console.log(loc.search);
console.log(clientObj);
console.log(session.meta);
console.log(session.token);
console.log(window.document.referrer);
*/
return OAUTH3.requests.grants(CONFIG.host, {
method: 'GET'
@ -231,7 +233,7 @@
console.log(grantResults);
if (grantResults.data.error) {
window.alert('grantResults: ' + grantResults.data.errorDescription || grantResults.data.error.message);
window.alert('grantResults: ' + grantResults.data.error_description || grantResults.data.error.message);
return;
}
@ -256,7 +258,7 @@
//return generateToken(session, clientObj);
}
grants = grantResults.originalData.grants.filter(function (grant) {
grants = (grantResults.originalData||grantResults.data).grants.filter(function (grant) {
if (clientUri === (grant.azp || grant.oauth_client_id || grant.oauthClientId)) {
return true;
}

View File

@ -27,6 +27,7 @@
};
// TODO move recase out
/*
oauth3._recaseRequest = function (recase, req) {
// convert JavaScript camelCase to oauth3/ruby snake_case
if (req.data && 'object' === typeof req.data) {
@ -44,6 +45,7 @@
}
return resp;
};
*/
oauth3.hooks = {
checkSession: function (preq, opts) {
@ -51,7 +53,7 @@
console.warn('[oauth3.hooks.checkSession] no session');
return oauth3.PromiseA.resolve(null);
}
var freshness = oauth3.core.jwt.getFreshness(preq.session.meta, opts.staletime);
var freshness = oauth3.core.jwt.getFreshness(preq.session.token, opts.staletime);
console.info('[oauth3.hooks.checkSession] freshness', freshness, preq.session);
switch (freshness) {
@ -118,11 +120,11 @@
oldSession.client_uri = clientUri; // azp
// info about the newly-discovered token
oldSession.meta = core.jwt.decode(oldSession.access_token).payload;
oldSession.token = oldSession.meta = core.jwt.decode(oldSession.access_token).payload;
oldSession.meta.sub = oldSession.meta.sub || oldSession.meta.acx.id;
oldSession.meta.client_uri = clientUri;
oldSession.meta.provider_uri = providerUri;
oldSession.token.sub = oldSession.token.sub || oldSession.token.acx.id;
oldSession.token.client_uri = clientUri;
oldSession.token.provider_uri = providerUri;
if (oldSession.refresh_token || oldSession.refreshToken) {
oldSession.refresh = core.jwt.decode(oldSession.refresh_token || oldSession.refreshToken).payload;
@ -193,14 +195,14 @@
// TODO simplify (nix recase)
oauth3.provideRequest = function (rawRequest, opts) {
opts = opts || {};
var Recase = exports.Recase || require('recase');
//var Recase = exports.Recase || require('recase');
// TODO make insensitive to providing exceptions
var recase = Recase.create({ exceptions: {} });
//var recase = Recase.create({ exceptions: {} });
function lintAndRequest(preq) {
function goGetHer() {
if (preq.session) {
// TODO check session.meta.aud against preq.url to make sure they match
// TODO check session.token.aud against preq.url to make sure they match
console.warn("[security] session audience checking has not been implemented yet (it's up to you to check)");
preq.headers = preq.headers || {};
preq.headers.Authorization = 'Bearer ' + (preq.session.access_token || preq.session.accessToken);
@ -236,9 +238,10 @@
return lintAndRequest(req, opts);
}
req = oauth3._recaseRequest(recase, req);
//req = oauth3._recaseRequest(recase, req);
return lintAndRequest(req, opts).then(function (res) {
return oauth3._recaseResponse(recase, res);
//return oauth3._recaseResponse(recase, res);
return res;
});
};
@ -285,7 +288,7 @@
return {
client: oauth3.hooks.getGrants(opts.client_id + '-client')
, grants: oauth3.hooks.getGrants(opts.client_id)
, grants: oauth3.hooks.getGrants(opts.client_id) || []
};
});
});
@ -295,10 +298,10 @@
var prequest = core.urls.loginCode(directive, opts);
return oauth3.request(prequest).then(function (res) {
// result = { uuid, expiresAt }
// result = { uuid, expires_at }
return {
otpUuid: res.data.uuid
, otpExpires: res.data.expiresAt
, otpExpires: res.data.expires_at
};
});
});

View File

@ -47,6 +47,9 @@
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;