diff --git a/README.md b/README.md index 78cb34c..ec7cb33 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,151 @@ 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 +If you have no idea what you're doing ------------ -**Easy Install** for Web Apps (including Mobile): +(people who know what they're doing should skip ahead to the tl;dr instructions) -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` +1. Create a folder for your project named after your app, such as `example.com/` +2. Inside of the folder `example.com/` a folder called `assets/` +3. Inside of the folder `example.com/assets` a folder called `org.oauth3/` +4. Download [oauth.js-v1.zip](https://git.daplie.com/Daplie/oauth3.js/repository/archive.zip?ref=v1) +5. Double-click to unzip the folder. +6. Copy the file `oauth3.core.js` into the folder `example.com/assets/org.oauth3/` +7. Copy the folder `well-known` into the folder `example.com/` +8. Rename the folder `well-known` to `.well-known` (when you do this, it become invisible, that's okay) +9. Add `` to your `index.html` +9. Add `` to your `index.html` +10. Create files in `example.com` called `app.js` and `index.html` and put this in it: + +`index.html`: +```html + + + + + + + + + + + + + + + +``` + +`app.js`: +```js +var OAUTH3 = window.OAUTH3; +var auth = OAUTH3.create(window.location); // use window.location to set Client URI (your app's id) + + +// this is any OAuth3-compatible provider, such as oauth3.org +// in v1.1.0 we'll add backwards compatibility for facebook.com, google.com, etc +// +function onChangeProvider(_providerUri) { + // example https://oauth3.org + return auth.setProvider(providerUri); +} + + +// This opens up the login window for the specified provider +// +function onClickLogin() { + + return auth.authenticate().then(function (session) { + + console.info('Authentication was Successful:'); + console.log(session); + + // You can use the PPID (or preferrably 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) + // 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); + + return auth.request({ + url: 'https://oauth3.org/api/org.oauth3.provider/inspect' + , session: session + }).then(function (resp) { + + console.info("Inspect Token:"); + console.log(resp.data); + + }); + + }, function (err) { + console.error('Authentication Failed:'); + console.log(err); + }); + +} + + +// This opens up the logout window +// +function onClickLogout() { + + return auth.logout().then(function () { + localStorage.clear(); + + console.info('Logout was Successful'); + + }, function (err) { + console.error('Logout Failed:'); + console.log(err); + }); + +} + + +// initialize the provider to be oauth3.org (or any compatible provider) +// +onChangeProvider('oauth3.org'); + + +$('body').on('click', '.js-login', onClickLogin); +$('body').on('click', '.js-logout', onClickLogout); +$('body').on('change', 'input.js-provider-uri', onChangeProvider); +``` + +Copy the `example.com/` folder to your webserver. + + +Example +------- + +If you had a simple website / webapp for `example.com` with only the most necessary files, +it might look like this: + +``` +example.com +│ +│ +├── .well-known (hidden) +│   └── oauth3 +│   ├── callback.html +│   ├── directives.json +│   └── index.html +├── assets +│   └── org.oauth3 +│   └── oauth3.core.js +│ +│ +├── css +│   └── main.css +├── index.html +└── js + └── app.js +``` + +Installation (if you know what you're doing) +------------ **Advanced Installation with `git`** @@ -63,40 +198,13 @@ ln -sf ../bower_components/oauth3/.well-known/oauth3 .well-known/oauth3 ln -sf ../bower_components/oauth3 assets/org.oauth3 ``` -Example -------- - -If you had a simple website / webapp for `example.com` with only the most necessary files, -it might look like this: - -``` -example.com -│ -│ -├── .well-known -│   └── oauth3 -│   ├── callback.html -│   ├── directives.json -│   └── index.html -├── assets -│   └── org.oauth3 -│   └── oauth3.implicit.js -│ -│ -├── css -│   └── main.css -├── index.html -└── js - └── app.js -``` - Usage ----- Update your HTML to include the the following script tag: ```html - + ``` You can create a very simple demo application like this: @@ -169,13 +277,46 @@ We've created an `Oauth3` service just for you: You can include that in addition to the standard file or, if you don't want an extra request, just paste it into your `app.js`. -Stable API +Simple API +---------- + +We include a small wrapper function of just a few lines in the bottom of `oauth3.core.js` +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 + // to create the Client URI (your app's id) + // and save it to an internal state + +promise = auth.init(location); // set and fetch your own site/app's configuration details +// promises your site's config + +promise = auth.setProvider(url); // changes the Provider URI (the site you're logging into), +// promises the provider's config // gets the config for that site (from their .well-known/oauth3), + // and caches it in internal state as the default + +promise = auth.authenticate(); // opens login window for the provider and returns a session + // (must be called after the setProvider promise has completed) + +promise = auth.authorize(permissions); // authenticates (if not authenticated) and opens a window to + // authorize a particular scope (contacts, photos, whatever) + +promise = auth.request({ url, method, data }); // make an (authorized) request to a provider's resource + // (contacts, photos, whatever) + +promise = auth.logout(); // opens logout window for the provider + +auth.session(); // returns the current session, if any +``` + + +Real API ---------- ``` -OAUTH3.utils.clientUri(window.location); // produces the default `client_uri` of your app (also used as `client_id`) +OAUTH3.clientUri(window.location); // produces the default `client_uri` of your app (also used as `client_id`) OAUTH3.discover(providerUri, { client_id: clientUri }); // Promises the config file for the provider and caches it in memory. @@ -203,7 +344,7 @@ OAUTH3.urls -Staging API +Core API (staging) ---------- These APIs are NOT yet public, stable APIs, but they are good to be aware of @@ -217,13 +358,24 @@ Public utilities for browser and node.js: OAUTH3.jwt .decode(''); // { iat, iss, aud, sub, exp, ttl } -OAUTH3.utils +OAUTH3 .query.stringify({ access_token: '...', debug: true }); // access_token=...&debug=true .scope.stringify([ 'profile', 'contacts' ]); // 'profile,contacts' .uri.normalize('https://oauth3.org/connect/'); // 'oauth3.org/connect' .url.normalize('oauth3.org/connect/'); // 'https://oauth3.org/connect' .url.resolve('oauth3.org/connect/', '/api/'); // 'https://oauth3.org/connect/api' - .atob(''); // '' (typically json ascii) +``` + +Issuer API (staging) +------------------- + +These additional methods are + +``` +OAUTH3 + .query.parse('#/?foo=bar&baz=qux'); // { access_token: '...', debug: 'true' } + .scope.parse('profile,contacts'); // [ 'profile', 'contacts' ] + .url.redirect(clientParams, grants, tokenOrError); // securely redirect to client (or give security or other error) ``` Internal API @@ -236,11 +388,12 @@ This APIs will absolutely change before they are made public OAUTH3.jwt .freshness(tokenMeta, staletimeSeconds, _now); // returns 'fresh', 'stale', or 'expired' (by seconds before expiry / ttl) -OAUTH3.utils +OAUTH3 .url._normalizePath('oauth3.org/connect/'); // 'oauth3.org/connect' - ._urlSafeBase64ToBase64(b64); // makes base64 safe for window.atob .randomState(); // a 128-bit crypto-random string ._insecureRandomState(); // a fallback for randomState() in old browsers + ._base64.atob(''); // '' (typically json ascii) + ._base64.decodeUrlSafe(b64); // makes base64 safe for window.atob and then calls atob OAUTH3._browser // a collection of things a browser needs to perform requests ``` diff --git a/oauth3.core.js b/oauth3.core.js index bc4ad35..15b2432 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -937,7 +937,7 @@ location = OAUTH3._browser.window.location; } - return { + var result = { _clientUri: OAUTH3.clientUri(location) , _providerUri: null , init: function (location) { @@ -1003,6 +1003,10 @@ return OAUTH3.logout(this._providerUri, opts); } }; + result.authenticate = result.login; + result.authorize = result.login; + result.expire = result.logout; + return result; }; }('undefined' !== typeof exports ? exports : window)); diff --git a/oauth3.implicit.js b/oauth3.implicit.js deleted file mode 100644 index bc4ad35..0000000 --- a/oauth3.implicit.js +++ /dev/null @@ -1,1008 +0,0 @@ -/* global Promise */ -;(function (exports) { - 'use strict'; - - var OAUTH3 = exports.OAUTH3 = { - clientUri: function (location) { - return OAUTH3.uri.normalize(location.host + location.pathname); - } - , error: { - parse: function (providerUri, params) { - var err = new Error(params.error_description || params.error.message || "Unknown error with provider '" + providerUri + "'"); - err.uri = params.error_uri || params.error.uri; - err.code = params.error.code || params.error; - return err; - } - } - , _base64: { - atob: function (base64) { - // atob must be called from the global context - // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome - return (exports.atob || require('atob'))(base64); - } - , decodeUrlSafe: 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 OAUTH3._base64.atob(b64); - } - } - , uri: { - normalize: function (uri) { - if ('string' !== typeof uri) { - console.error((new Error('stack')).stack); - } - // tested with - // example.com - // example.com/ - // http://example.com - // https://example.com/ - return uri - .replace(/^(https?:\/\/)?/i, '') - .replace(/\/?$/, '') - ; - } - } - , url: { - normalize: function (url) { - if ('string' !== typeof url) { - console.error((new Error('stack')).stack); - } - // tested with - // example.com - // example.com/ - // http://example.com - // https://example.com/ - return url - .replace(/^(https?:\/\/)?/i, 'https://') - .replace(/\/?$/, '') - ; - } - , resolve: function (base, next) { - if (/^https:\/\//i.test(next)) { - return next; - } - return this.normalize(base) + '/' + this._normalizePath(next); - } - , _normalizePath: function (path) { - return path.replace(/^\//, '').replace(/\/$/, ''); - } - } - , query: { - stringify: function (params) { - var qs = []; - - Object.keys(params).forEach(function (key) { - // TODO nullify instead? - if ('undefined' === typeof params[key]) { - return; - } - - if ('scope' === key) { - params[key] = OAUTH3.scope.stringify(params[key]); - } - - qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key])); - }); - - return qs.join('&'); - } - } - , scope: { - stringify: function (scope) { - if (Array.isArray(scope)) { - scope = scope.join(' '); - } - return scope; - } - } - , randomState: function () { - // 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) { - return OAUTH3.utils._insecureRandomState(); - } - } - , _insecureRandomState: function () { - var i; - var ch; - var str; - // 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; - } - , 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 b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64); - return b64; - }); - - return { - header: JSON.parse(jsons[0]) - , payload: JSON.parse(jsons[1]) - , signature: parts[2] // should remain url-safe base64 - }; - } - , freshness: function (tokenMeta, staletime, _now) { - staletime = staletime || (15 * 60); - var 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'; - } - } - , 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 = OAUTH3.url.normalize(opts.client_id || opts.client_uri); - providerUri = OAUTH3.url.normalize(providerUri); - - var params = { - action: 'directives' - , state: opts.state || OAUTH3.utils.randomState() - , redirect_uri: clientId + (opts.client_callback_path || '/.well-known/oauth3/callback.html#/') - , response_type: 'rpc' - , _method: 'GET' - , _pathname: '.well-known/oauth3/directives.json' - , debug: opts.debug || undefined - }; - - var result = { - url: providerUri + '/.well-known/oauth3/#/?' + OAUTH3.query.stringify(params) - , state: params.state - , method: 'GET' - , query: params - }; - - return result; - } - , implicitGrant: function (directive, opts) { - // - // Example Implicit Grant Request - // (for generating a browser-only session, not a session on your server) - // - // GET https://example.com/api/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 scope = opts.scope || directive.authn_scope; - var args = directive[type]; - var uri = args.url; - var state = opts.state || OAUTH3.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 - , state: state - }; - var result; - - params.response_type = responseType; - if (scope) { - params.scope = OAUTH3.scope.stringify(scope); - } - if (!opts.redirect_uri) { - // TODO consider making this optional - //console.warn("auto-generating redirect_uri from hard-coded callback.html" - // + " (should be configurable... but then redirect_uri could just be manually-generated)"); - opts.redirect_uri = OAUTH3.url.resolve( - OAUTH3.url.normalize(params.client_uri) - , '.well-known/oauth3/callback.html' - ); - } - params.redirect_uri = opts.redirect_uri; - - uri += '?' + OAUTH3.query.stringify(params); - - result = { - url: uri - , state: state - , method: args.method - , query: params - }; - - return result; - } - , 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": "<>", "scope": "<>" - // , "username": "<>", "password": "password" } - // - opts = opts || {}; - var type = 'access_token'; - var grantType = 'refresh_token'; - - var scope = opts.scope || directive.authn_scope; - var clientSecret = opts.client_secret; - var args = directive[type]; - var params = { - "grant_type": grantType - , "refresh_token": opts.refresh_token || (opts.session && opts.session.refresh_token) - , "response_type": 'token' - , "client_id": opts.client_id || opts.client_uri - , "client_uri": opts.client_uri - //, "scope": undefined - //, "client_secret": undefined - , debug: opts.debug || undefined - }; - var uri = args.url; - var body; - - if (clientSecret) { - // TODO not allowed in the browser - console.warn("if this is a browser, you must not use client_secret"); - params.client_secret = clientSecret; - } - - if (scope) { - params.scope = OAUTH3.scope.stringify(scope); - } - - if ('GET' === args.method.toUpperCase()) { - uri += '?' + OAUTH3.query.stringify(params); - } else { - body = params; - } - - return { - url: uri - , method: args.method - , data: body - }; - } - , logout: function (directive, opts) { - // action=logout - - // Example Logout Request - // (generally for 1st or 3rd party server-side, mobile, and desktop apps) - // - // GET https://example.com/#/logout/ - // ?client_id=<> - // &access_token=<> - // &sub=<> - // - // Note that the use of # keeps certain parameters from traveling across - // the network at all (and we use https anyway) - // - opts = opts || {}; - var action = 'logout'; - var args = directive[action]; - var state = opts.state || OAUTH3.utils.randomState(); - var params = { - action: action - //, response_type: 'confirmation' - , client_id: opts.client_id || opts.client_uri - , client_uri: opts.client_uri || opts.client_id - , state: state - , redirect_uri: opts.redirect_uri = OAUTH3.url.resolve( - OAUTH3.url.normalize(opts.client_uri || opts.client_id) - , '.well-known/oauth3/callback.html' - ) - , debug: opts.debug - }; - var uri = args.url; - var body; - - if ('GET' === args.method.toUpperCase()) { - uri += '?' + OAUTH3.query.stringify(params); - } else { - body = params; - } - - return { - url: OAUTH3.url.resolve(directive.issuer, uri) - , method: args.method - , state: state - , data: body - }; - } - } - , hooks: { - directives: { - _get: function (providerUri) { - providerUri = OAUTH3.uri.normalize(providerUri); - if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives._cache[providerUri] - || OAUTH3.hooks.directives.get(providerUri)) - .then(function (directives) { - // or do .then(this._set) to keep DRY? - OAUTH3.hooks.directives._cache[providerUri] = directives; - return directives; - }); - } - , _getCached: function (providerUri) { - providerUri = OAUTH3.uri.normalize(providerUri); - return OAUTH3.hooks.directives._cache[providerUri]; - } - , get: function (providerUri) { - console.warn('[Warn] You should implement: OAUTH3.hooks.directives.get = function (providerUri) { return directives; }'); - return JSON.parse(window.localStorage.getItem('directives-' + providerUri) || '{}'); - } - , _set: function (providerUri, directives) { - providerUri = OAUTH3.uri.normalize(providerUri); - if (!OAUTH3.hooks.directives._cache) { OAUTH3.hooks.directives._cache = {}; } - OAUTH3.hooks.directives._cache[providerUri] = directives; - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.directives.set(providerUri, directives)); - } - , set: function (providerUri, directives) { - console.warn('[Warn] You should implement: OAUTH3.hooks.directives.set = function (providerUri, directives) { return directives; }'); - window.localStorage.setItem('directives-' + providerUri, JSON.stringify(directives)); - return directives; - } - } - , session: { - refresh: function (oldSession, newSession) { - var providerUri = oldSession.provider_uri; - var clientUri = oldSession.client_uri; - - 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 = OAUTH3.jwt.decode(oldSession.access_token).payload; - - 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.refresh = OAUTH3.jwt.decode(oldSession.refresh_token).payload; - oldSession.refresh.sub = oldSession.refresh.sub || oldSession.refresh.acx.id; - oldSession.refresh.provider_uri = providerUri; - } - - // set for a set of audiences - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session.set(providerUri, oldSession)); - } - , check: function (preq, opts) { - if (!preq.session) { - return OAUTH3.PromiseA.resolve(null); - } - var freshness = OAUTH3.jwt.freshness(preq.session.token, opts.staletime); - - switch (freshness) { - case 'stale': - return OAUTH3.hooks.session.stale(preq.session); - case 'expired': - return OAUTH3.hooks.session.expired(preq.session).then(function (newSession) { - preq.session = newSession; - return newSession; - }); - //case 'fresh': - default: - return OAUTH3.PromiseA.resolve(preq.session); - } - } - , stale: function (staleSession) { - if (OAUTH3.hooks.session._stalePromise) { - return OAUTH3.PromiseA.resolve(staleSession); - } - - OAUTH3.hooks.session._stalePromise = OAUTH3._refreshToken( - staleSession.provider_uri - , { client_uri: staleSession.client_uri - , session: staleSession - , debug: staleSession.debug - } - ).then(function (newSession) { - OAUTH3.hooks.session._stalePromise = null; - return newSession; // oauth3.hooks.refreshSession(staleSession, newSession); - }, function () { - OAUTH3.hooks.session._stalePromise = null; - }); - - return OAUTH3.PromiseA.resolve(staleSession); - } - , expired: function (expiredSession) { - return OAUTH3._refreshToken( - expiredSession.provider_uri - , { client_uri: expiredSession.client_uri - , session: expiredSession - , debug: expiredSession.debug - } - ).then(function (newSession) { - return newSession; // oauth3.hooks.refreshSession(expiredSession, newSession); - }); - } - , set: function (providerUri, newSession) { - if (!providerUri) { - console.error(new Error('no providerUri').stack); - throw new Error("providerUri is not set"); - } - providerUri = OAUTH3.uri.normalize(providerUri); - console.warn('[Warn] Please implement OAUTH3.hooks.session.set = function (providerUri, newSession) { return PromiseA; }'); - if (!OAUTH3.hooks.session._sessions) { OAUTH3.hooks.session._sessions = {}; } - OAUTH3.hooks.session._sessions[providerUri] = newSession; - return OAUTH3.PromiseA.resolve(newSession); - } - , _getCached: function (providerUri) { - providerUri = OAUTH3.uri.normalize(providerUri); - return OAUTH3.hooks.session._sessions[providerUri]; - } - , get: function (providerUri) { - providerUri = OAUTH3.uri.normalize(providerUri); - if (!providerUri) { - throw new Error("providerUri is not set"); - } - console.warn('[Warn] Please implement OAUTH3.hooks.session.get = function (providerUri) { return PromiseA; }'); - if (!OAUTH3.hooks.session._sessions) { OAUTH3.hooks.session._sessions = {}; } - return OAUTH3.PromiseA.resolve(OAUTH3.hooks.session._sessions[providerUri]); - } - } - } - , discover: function (providerUri, opts) { - if (!providerUri) { - throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri); - } - - return OAUTH3.hooks.directives._get(providerUri).then(function (directives) { - if (directives && directives.issuer) { - return directives; - } - return OAUTH3._discoverHelper(providerUri, opts).then(function (directives) { - directives.azp = directives.azp || OAUTH3.url.normalize(providerUri); - directives.issuer = directives.issuer || OAUTH3.url.normalize(providerUri); - // OAUTH3.PromiseA.resolve() is taken care of because this is wrapped - return OAUTH3.hooks.directives._set(providerUri, directives); - }); - }); - } - , _discoverHelper: function(providerUri, opts) { - return OAUTH3._browser.discover(providerUri, opts); - } - , request: function (preq, opts) { - function fetch() { - 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 || preq.session.accessToken); - } - - return OAUTH3._requestHelper(preq, opts); - } - - OAUTH3.url.resolve(preq.providerUri || preq.provider_uri || preq.directives && preq.directives.issuer, preq.url); - - if (!preq.session) { - return fetch(); - } - - return OAUTH3.hooks.session.check(preq, opts).then(fetch); - } - , _requestHelper: function (preq, opts) { - return OAUTH3._browser.request(preq, opts); - } - , implicitGrant: function(directives, opts) { - var promise; - var providerUri = directives.azp || directives.issuer || directives; - - if (opts.broker) { - // Discovery can happen in-flow because we know that this is - // a valid oauth3 provider - promise = OAUTH3._discoverThenImplicitGrant(providerUri, opts); - } - else { - // Discovery must take place before calling implicitGrant - promise = OAUTH3._implicitGrant(OAUTH3.hooks.directives._getCached(providerUri), opts); - } - - return promise.then(function (tokens) { - // TODO abstract browser bits away - try { - OAUTH3._browser.closeFrame(tokens.state || opts._state, opts); - } catch(e) { - console.warn("[implicitGrant] TODO abstract browser bits away"); - } - opts._state = undefined; - return OAUTH3.hooks.session.refresh( - opts.session || { - provider_uri: providerUri - , client_id: opts.client_id - , client_uri: opts.client_uri || opts.clientUri - } - , tokens - ); - }); - } - , _discoverThenImplicitGrant: function(providerUri, opts) { - opts.windowType = opts.windowType || 'popup'; - return OAUTH3.discover(providerUri, opts).then(function (directives) { - return OAUTH3._implicitGrant(directives, opts).then(function (tokens) { - return tokens; - }); - }); - } - , _implicitGrant: function(directives, opts) { - // TODO this may need to be synchronous for browser security policy - // Do some stuff - var authReq = OAUTH3.urls.implicitGrant( - directives - , { redirect_uri: opts.redirect_uri - , client_id: opts.client_id || opts.client_uri - , client_uri: opts.client_uri || opts.client_id - , state: opts._state || undefined - , debug: opts.debug - } - ); - - if (opts.debug) { - window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!"); - } - - return OAUTH3._browser.frameRequest( - OAUTH3.url.resolve(directives.issuer, authReq.url) - , authReq.state // state should recycle params - , { windowType: opts.windowType - , reuseWindow: opts.broker && '-broker' - , debug: opts.debug - } - ).then(function (tokens) { - if (tokens.error) { - // TODO directives.audience - return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, tokens)); - } - - return tokens; - }); - } - , _refreshToken: function (providerUri, opts) { - return OAUTH3.discover(providerUri, opts).then(function (directive) { - var prequest = OAUTH3.urls.refreshToken(directive, opts); - - return OAUTH3.request(prequest).then(function (req) { - var data = req.data; - data.provider_uri = providerUri; - if (data.error) { - return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, data)); - } - return OAUTH3.hooks.session.refresh(opts, data); - }); - }); - } - , logout: function(providerUri, opts) { - return OAUTH3._logoutHelper(OAUTH3.hooks.directives._getCached(providerUri), opts); - } - , _logoutHelper: function(directives, opts) { - var logoutReq = OAUTH3.urls.logout( - directives - , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) - , windowType: 'popup' // we'll figure out background later - , broker: opts.broker - //, state: opts._state - , debug: opts.debug - } - ); - - return OAUTH3._browser.frameRequest( - OAUTH3.url.resolve(directives.issuer, logoutReq.url) - , logoutReq.state // state should recycle params - , { windowType: 'popup' - , reuseWindow: opts.broker && '-broker' - , debug: opts.debug - } - ).then(function (params) { - OAUTH3._browser.closeFrame(params.state || opts._state, opts); - - if (params.error) { - // TODO directives.audience - return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); - } - - return params; - }); - } - - - // - // Let the Code Waste begin!! - // - , _browser: { - window: window - // TODO we don't need to include this if we're using jQuery or angular - , discover: function(providerUri, opts) { - opts = opts || {}; - providerUri = OAUTH3.url.normalize(providerUri); - - if (providerUri.match(OAUTH3._browser.window.location.hostname)) { - 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.url.normalize(providerUri) + '/.well-known/oauth3/directives.json' - }).then(function (resp) { - return resp.data; - }); - } - - if (!(opts.client_id || opts.client_uri).match(OAUTH3._browser.window.location.hostname)) { - console.warn("It looks like your client_id doesn't match your current window..." - + " this probably won't end well"); - console.warn(opts.client_id || opts.client_uri, OAUTH3._browser.window.location.hostname); - } - - var discReq = OAUTH3.urls.discover( - providerUri - , { client_id: (opts.client_id || opts.client_uri || OAUTH3.clientUri(OAUTH3._browser.window.location)) - , windowType: opts.broker && opts.windowType || 'background' - , broker: opts.broker - , state: opts._state || undefined - , debug: opts.debug - } - ); - opts._state = discReq.state; - //var discReq = OAUTH3.urls.discover(providerUri, opts); - - // hmm... we're gonna need a broker for this since switching windows is distracting, - // popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS - // 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 - // TODO allow node to open a desktop browser window - opts._windowType = opts.windowType; - opts.windowType = opts.windowType || 'background'; - return OAUTH3._browser.frameRequest( - OAUTH3.url.resolve(providerUri, discReq.url) - , discReq.state - // why not just pass opts whole? - , { windowType: opts.windowType - , reuseWindow: opts.broker && '-broker' - , debug: opts.debug - } - ).then(function (params) { - opts.windowType = opts._windowType; - - // caller will call OAUTH3._browser.closeFrame(discReq.state, { debug: opts.debug || params.debug }); - if (params.error) { - // TODO directives.issuer || directives.audience - return OAUTH3.PromiseA.reject(OAUTH3.error.parse(providerUri, params)); - } - - // TODO params should have response_type indicating json, binary, etc - var directives = JSON.parse(OAUTH3._base64.decodeUrlSafe(params.result || params.directives)); - // caller will call OAUTH3.hooks.directives._set(providerUri, directives); - return directives; - }); - } - , request: function (preq, _sys) { - return new OAUTH3.PromiseA(function (resolve, reject) { - var xhr; - try { - xhr = new XMLHttpRequest(_sys); - } catch(e) { - xhr = new XMLHttpRequest(); - } - xhr.onreadystatechange = function () { - var data; - if (xhr.readyState !== XMLHttpRequest.DONE) { - // nothing to do here - return; - } - - if (xhr.status !== 200) { - reject(new Error('bad status code: ' + xhr.status)); - return; - } - - try { - data = JSON.parse(xhr.responseText); - } catch(e) { - data = xhr.responseText; - } - - resolve({ - request: xhr - , data: data - , status: xhr.status - }); - }; - xhr.open(preq.method, preq.url, true); - var headers = preq.headers || {}; - Object.keys(headers).forEach(function (key) { - xhr.setRequestHeader(key, headers[key]); - }); - xhr.send(); - }); - } - , frameRequest: function (url, state, opts) { - opts = opts || {}; - var previousFrame = OAUTH3._browser._frames[state]; - - var windowType = opts.windowType; - if (!windowType) { - windowType = 'popup'; - } - - var timeout = opts.timeout; - if (opts.debug) { - timeout = timeout || 3 * 60 * 1000; - } - else { - timeout = timeout || ('background' === windowType ? 15 * 1000 : 3 * 60 * 1000); - } - - return new OAUTH3.PromiseA(function (resolve, reject) { - // TODO periodically garbage collect expired handlers from window object - var tok; - - function cleanup() { - delete window['--oauth3-callback-' + state]; - clearTimeout(tok); - tok = null; - // the actual close is done later (by the caller) so that the window/frame - // can be reused or self-closes synchronously itself / by parent - // (probably won't ever happen, but that's a negotiable implementation detail) - } - - - window['--oauth3-callback-' + state] = function (params) { - resolve(params); - cleanup(); - }; - - tok = setTimeout(function () { - var err = new Error( - "the '" + windowType + "' request did not complete within " + Math.round(timeout / 1000) + "s" - ); - err.code = "E_TIMEOUT"; - reject(err); - cleanup(); - }, timeout); - - setTimeout(function () { - if (!OAUTH3._browser._frames[state]) { - reject(new Error("TODO: open the iframe first and discover oauth3 directives before popup")); - cleanup(); - } - }, 0); - - if ('background' === windowType) { - if (previousFrame) { - previousFrame.location = url; - //promise = previousFrame.promise; - } - else { - OAUTH3._browser._frames[state] = OAUTH3._browser.iframe(url, state, opts); - } - } else if ('popup' === windowType) { - if (previousFrame) { - previousFrame.location = url; - if (opts.debug) { - previousFrame.focus(); - } - } - else { - OAUTH3._browser._frames[state] = OAUTH3._browser.frame(url, state, opts); - } - } else if ('inline' === windowType) { - // callback function will never execute and would need to redirect back to current page - // rather than the callback.html - url += '&original_url=' + OAUTH3._browser.window.location.href; - OAUTH3._browser.window.location = url; - //promise = OAUTH3.PromiseA.resolve({ url: url }); - return; - } else { - throw new Error("login framing method options.windowType=" - + opts.windowType + " not type yet implemented"); - } - - }).then(function (params) { - if (params.error) { - // TODO directives.issuer || directives.audience - return OAUTH3.PromiseA.reject(OAUTH3.error.parse('https://oauth3.org', params)); - } - return params; - }); - } - , closeFrame: function (state, opts) { - opts = opts || {}; - function close() { - try { - OAUTH3._browser._frames[state].close(); - } catch(e) { - try { - OAUTH3._browser._frames[state].remove(); - } catch(e) { - console.error(new Error("Could not clase window/iframe. closeFrame may have been called twice.")); - } - } - - delete OAUTH3._browser._frames[state]; - } - - if (opts.debug) { - if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) { - close(); - } - } - else { - close(); - } - } - , _frames: {} - , iframe: function (url, state, opts) { - var framesrc = '