handle accounts
This commit is contained in:
		
							parent
							
								
									c045e4c712
								
							
						
					
					
						commit
						4e459ea617
					
				
							
								
								
									
										90
									
								
								lib/extensions/admin/account.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								lib/extensions/admin/account.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Telebit Account</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <form class="js-auth-form"> | ||||
|       <input class="js-auth-subject" type="email"/> | ||||
|       <button class="js-auth-submit" type="submit">Login</button> | ||||
|     </form> | ||||
| 
 | ||||
|     <script src="assets/oauth3.org/oauth3.core.js"></script> | ||||
|     <script> | ||||
|       (function () { | ||||
|         'use strict'; | ||||
|         var OAUTH3 = window.OAUTH3; | ||||
|         var oauth3 = OAUTH3.create({ | ||||
|           host: window.location.host | ||||
|         , pathname: window.location.pathname.replace(/\/[^\/]*$/, '/') | ||||
|         }); | ||||
|         var $ = function () { return document.querySelector.apply(document, arguments); } | ||||
| 
 | ||||
| 				function onChangeProvider(providerUri) { | ||||
| 					// example https://oauth3.org | ||||
| 					return oauth3.setIdentityProvider(providerUri); | ||||
| 				} | ||||
| 
 | ||||
| 				// This opens up the login window for the specified provider | ||||
| 				// | ||||
| 				function onClickLogin(ev) { | ||||
|           ev.preventDefault(); | ||||
|           ev.stopPropagation(); | ||||
| 
 | ||||
|           // TODO check subject for provider viability | ||||
|           return oauth3.authenticate({ | ||||
|             subject: $('.js-auth-subject').value | ||||
|           }).then(function (session) { | ||||
| 
 | ||||
| 						console.info('Authentication was Successful:'); | ||||
| 						console.log(session); | ||||
| 
 | ||||
| 						// 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) | ||||
| 						// 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 oauth3.request({ | ||||
| 							url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json' | ||||
|                 .replace(/:sub/g, session.token.sub) | ||||
|                 .replace(/:kid/g, session.token.iss) | ||||
| 						, session: session | ||||
| 						}).then(function (resp) { | ||||
|               console.info("Public Key:"); | ||||
|               console.log(resp.data); | ||||
| 
 | ||||
|               return oauth3.request({ | ||||
|                 url: 'https://api.oauth3.org/api/issuer@oauth3.org/acl/profile' | ||||
|               , session: session | ||||
|               }).then(function (resp) { | ||||
| 
 | ||||
|                 console.info("Inspect Token:"); | ||||
|                 console.log(resp.data); | ||||
| 
 | ||||
|                 return oauth3.request({ | ||||
|                   url: 'https://api.telebit.cloud/api/telebit.cloud/account' | ||||
|                 , session: session | ||||
|                 }).then(function (resp) { | ||||
| 
 | ||||
|                   console.info("Telebit Account:"); | ||||
|                   console.log(resp.data); | ||||
| 
 | ||||
|                 }); | ||||
| 
 | ||||
| 
 | ||||
|               }); | ||||
| 
 | ||||
| 						}); | ||||
| 
 | ||||
| 					}, function (err) { | ||||
| 						console.error('Authentication Failed:'); | ||||
| 						console.log(err); | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
|         $('body form.js-auth-form').addEventListener('submit', onClickLogin); | ||||
|         onChangeProvider('oauth3.org'); | ||||
|       }()); | ||||
|     </script> | ||||
|   </body> | ||||
| </html> | ||||
| @ -29,6 +29,9 @@ | ||||
|       <p>Friends enable friends to share anything, access anywhere, connect anytime.</p> | ||||
|     </center> | ||||
| 
 | ||||
|     <a href="account.html#/login">Login</a> | ||||
|     <a href="account.html#/create_account">Create Account</a> | ||||
| 
 | ||||
|     <div style="width: 800px; margin: auto;"> | ||||
|       <div> | ||||
|         <h2>Share and Test over HTTPS</h2> | ||||
| @ -139,74 +142,6 @@ TCP | ||||
| 
 | ||||
|     </div> | ||||
| 
 | ||||
|     <form class="js-auth-form"> | ||||
|       <input class="js-auth-subject" type="email"/> | ||||
|       <button class="js-auth-submit" type="submit">Login</button> | ||||
|     </form> | ||||
|     <script src="assets/oauth3.org/oauth3.core.js"></script> | ||||
|     <script> | ||||
|       (function () { | ||||
|         'use strict'; | ||||
|         var OAUTH3 = window.OAUTH3; | ||||
|         var oauth3 = OAUTH3.create(window.location); | ||||
|         var $ = function () { return document.querySelector.apply(document, arguments); } | ||||
| 
 | ||||
| 				function onChangeProvider(providerUri) { | ||||
| 					// example https://oauth3.org | ||||
| 					return oauth3.setIdentityProvider(providerUri); | ||||
| 				} | ||||
| 
 | ||||
| 				// This opens up the login window for the specified provider | ||||
| 				// | ||||
| 				function onClickLogin(ev) { | ||||
|           ev.preventDefault(); | ||||
|           ev.stopPropagation(); | ||||
| 
 | ||||
|           // TODO check subject for provider viability | ||||
|           return oauth3.authenticate({ | ||||
|             subject: $('.js-auth-subject').value | ||||
|           }).then(function (session) { | ||||
| 
 | ||||
| 						console.info('Authentication was Successful:'); | ||||
| 						console.log(session); | ||||
| 
 | ||||
| 						// 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) | ||||
| 						// 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 oauth3.request({ | ||||
| 							url: 'https://api.oauth3.org/api/issuer@oauth3.org/jwks/:sub/:kid.json' | ||||
|                 .replace(/:sub/g, session.token.sub) | ||||
|                 .replace(/:kid/g, session.token.iss) | ||||
| 						, session: session | ||||
| 						}).then(function (resp) { | ||||
|               console.info("Public Key:"); | ||||
|               console.log(resp.data); | ||||
| 
 | ||||
|               return oauth3.request({ | ||||
|                 url: 'https://api.oauth3.org/api/issuer@oauth3.org/acl/profile' | ||||
|               , session: session | ||||
|               }).then(function (resp) { | ||||
| 
 | ||||
|                 console.info("Inspect Token:"); | ||||
|                 console.log(resp.data); | ||||
| 
 | ||||
|               }); | ||||
| 
 | ||||
| 						}); | ||||
| 
 | ||||
| 					}, function (err) { | ||||
| 						console.error('Authentication Failed:'); | ||||
| 						console.log(err); | ||||
| 					}); | ||||
| 
 | ||||
| 				} | ||||
|         $('body form.js-auth-form').addEventListener('submit', onClickLogin); | ||||
|         onChangeProvider('oauth3.org'); | ||||
|       }()); | ||||
|     </script> | ||||
|     <script src="js/app.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| @ -6,7 +6,9 @@ var util = require('util'); | ||||
| var crypto = require('crypto'); | ||||
| var escapeHtml = require('escape-html'); | ||||
| var jwt = require('jsonwebtoken'); | ||||
| var requestAsync = util.promisify(require('request')); | ||||
| var requestAsync = util.promisify(require('@coolaj86/urequest')); | ||||
| var readFileAsync = util.promisify(fs.readFile); | ||||
| var mkdirpAsync = util.promisify(require('mkdirp')); | ||||
| 
 | ||||
| var _auths = module.exports._auths = {}; | ||||
| var Auths = {}; | ||||
| @ -77,6 +79,72 @@ Auths._clean = function () { | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| var sfs = require('safe-replace'); | ||||
| var Accounts = {}; | ||||
| Accounts._getTokenId = function (auth) { | ||||
|   return auth.data.sub + '@' + (auth.data.iss||'').replace(/\/|\\/g, '-'); | ||||
| }; | ||||
| Accounts._accPath = function (req, accId) { | ||||
|   return path.join(req._state.config.accountsDir, 'self', accId); | ||||
| }; | ||||
| Accounts._subPath = function (req, id) { | ||||
|   return path.join(req._state.config.accountsDir, 'oauth3', id); | ||||
| }; | ||||
| Accounts._setSub = function (req, id, subData) { | ||||
|   var subpath = Accounts._subPath(req, id); | ||||
|   return mkdirpAsync(subpath).then(function () { | ||||
|     return sfs.writeFileAsync(path.join(subpath, 'index.json'), JSON.stringify(subData)); | ||||
|   }); | ||||
| }; | ||||
| Accounts._setAcc = function (req, accId, acc) { | ||||
|   var accpath = Accounts._accPath(req, accId); | ||||
|   return mkdirpAsync(accpath).then(function () { | ||||
|     return sfs.writeFileAsync(path.join(accpath, 'index.json'), JSON.stringify(acc)); | ||||
|   }); | ||||
| }; | ||||
| Accounts.create = function (req) { | ||||
|   var id = Accounts._getTokenId(req.auth); | ||||
|   var acc = { | ||||
|     sub: crypto.randomBytes(16).toString('hex') | ||||
|          // TODO use something from the request to know which of the domains to use
 | ||||
|   , iss: req._state.config.webminDomain | ||||
|   , contacts: [] | ||||
|   }; | ||||
|   var accId = Accounts._getTokenId(acc); | ||||
|   acc.id = accId; | ||||
| 
 | ||||
|   // TODO notify any non-authorized accounts that they've been added?
 | ||||
|   return Accounts.getBySub(req).then(function (subData) { | ||||
|     subData.accounts.push({ type: 'self', id: accId }); | ||||
|     acc.contacts.push({ type: 'oauth3', id: subData.id, sub: subData.sub, iss: subData.iss }); | ||||
|     return Accounts._setSub(req, id, subData).then(function () { | ||||
|       return Accounts._setAcc(req, accId, acc).then(function () { | ||||
|         return acc; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| /* | ||||
| // TODO an owner of an asset can give permission to another entity
 | ||||
| // but that does not mean that that owner has access to that entity's things
 | ||||
| // Example:
 | ||||
| //   A 3rd party login's email verification cannot be trusted for auth
 | ||||
| //   Only 1st party verifications can be trusted for authorization
 | ||||
| Accounts.link = function (req) { | ||||
| }; | ||||
| */ | ||||
| Accounts.getBySub = function (req) { | ||||
|   var id = Accounts._getTokenId(req.auth); | ||||
|   var subpath = Accounts._subPath(req, id); | ||||
|   return readFileAsync(path.join(subpath, 'index.json'), 'utf8').then(function (text) { | ||||
|     return JSON.parse(text); | ||||
|   }, function (/*err*/) { | ||||
|     return null; | ||||
|   }).then(function (links) { | ||||
|     return links || { id: id, sub: req.auth.sub, iss: req.auth.iss, accounts: [] }; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function sendMail(state, auth) { | ||||
|   console.log('[DEBUG] ext auth', auth); | ||||
|   /* | ||||
| @ -137,6 +205,179 @@ function sendMail(state, auth) { | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| // TODO replace with OAuth3 function
 | ||||
| function oauth3Auth(req, res, next) { | ||||
|   var jwt = require('jsonwebtoken'); | ||||
|   var verifyJwt = util.promisify(jwt.verify); | ||||
|   var token = (req.headers.authorization||'').replace(/^bearer /i, ''); | ||||
|   var auth; | ||||
|   var authData; | ||||
| 
 | ||||
|   if (!token) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: "E_NOAUTH" | ||||
|       , message: "no authorization header" | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     authData = jwt.decode(token, { complete: true }); | ||||
|   } catch(e) { | ||||
|     authData = null; | ||||
|   } | ||||
| 
 | ||||
|   if (!authData) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: "E_PARSEAUTH" | ||||
|       , message: "could not parse authorization header as JWT" | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   auth = authData.payload; | ||||
|   if (!auth.sub && ('*' === auth.aud || '*' === auth.azp)) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: "E_NOIMPL" | ||||
|       , message: "missing 'sub' and a wildcard 'azp' or 'aud' indicates that this is an exchange token," | ||||
|          + " however, this app has not yet implemented opaque token exchange" | ||||
|       } | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if ([ 'sub', 'iss' ].some(function (key) { | ||||
|     if ('string' !== typeof auth[key]) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_PARSEAUTH" | ||||
|         , message: "could not read property '" + key + "' of authorization token" | ||||
|         } | ||||
|       }); | ||||
|       return true; | ||||
|     } | ||||
|   })) { return; } | ||||
|   if ([ 'kid' ].some(function (key) { | ||||
|     if (/\/|\\/.test(authData.header[key])) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_PARSESUBJECT" | ||||
|         , message: "'" + key + "' `" + JSON.stringify(authData.header[key]) + "' contains invalid characters" | ||||
|         } | ||||
|       }); | ||||
|       return true; | ||||
|     } | ||||
|   })) { return; } | ||||
|   if ([ 'sub', 'kid' ].some(function (key) { | ||||
|     if (/\/|\\/.test(auth[key])) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_PARSESUBJECT" | ||||
|         , message: "'" + key + "' `" + JSON.stringify(auth[key]) + "' contains invalid characters" | ||||
|         } | ||||
|       }); | ||||
|       return true; | ||||
|     } | ||||
|   })) { return; } | ||||
| 
 | ||||
|   // TODO needs to work with app:// and custom://
 | ||||
|   function prefixHttps(str) { | ||||
|     return (str||'').replace(/^(https?:\/\/)?/i, 'https://'); | ||||
|   } | ||||
| 
 | ||||
|   var url = require('url'); | ||||
|   var discoveryUrl = url.resolve(prefixHttps(auth.iss), '_apis/oauth3.org/index.json'); | ||||
|   console.log('discoveryUrl: ', discoveryUrl, auth.iss); | ||||
|   return requestAsync({ | ||||
|     url: discoveryUrl | ||||
|   , json: true | ||||
|   }).then(function (resp) { | ||||
| 
 | ||||
|     // TODO
 | ||||
|     // it may be necessary to exchange the token,
 | ||||
| 
 | ||||
|     if (200 !== resp.statusCode || 'object' !== typeof resp.body || !resp.body.retrieve_jwk | ||||
|         || 'string' !== typeof resp.body.retrieve_jwk.url || 'string' !== typeof resp.body.api) { | ||||
|       res.send({ | ||||
|         error: { | ||||
|           code: "E_NOTFOUND" | ||||
|         , message: resp.statusCode + ": issuer `" + JSON.stringify(auth.iss) | ||||
|             + "' does not declare 'api' & 'retrieve_key' and hence the token you provided cannot be verified." | ||||
|         , _status: resp.statusCode | ||||
|         , _url: discoveryUrl | ||||
|         , _body: resp.body | ||||
|         } | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     var keyUrl = url.resolve( | ||||
|         prefixHttps(resp.body.api).replace(/:hostname/g, auth.iss) | ||||
|       , resp.body.retrieve_jwk.url | ||||
|           .replace(/:hostname/g, auth.iss) | ||||
|           .replace(/:sub/g, auth.sub) | ||||
|           // TODO
 | ||||
|           .replace(/:kid/g, authData.header.kid || auth.iss) | ||||
|     ); | ||||
|     console.log('keyUrl: ', keyUrl); | ||||
|     return requestAsync({ | ||||
|       url: keyUrl | ||||
|     , json: true | ||||
|     }).then(function (resp) { | ||||
|       var jwk = resp.body; | ||||
|       if (200 !== resp.statusCode || 'object' !== typeof resp.body) { | ||||
|         //headers.authorization
 | ||||
|         res.send({ | ||||
|           error: { | ||||
|             code: "E_NOTFOUND" | ||||
|           , message: resp.statusCode + ": did not retrieve public key from `" + JSON.stringify(auth.iss) | ||||
|               + "' for token validation and hence the token you provided cannot be verified." | ||||
|           , _status: resp.statusCode | ||||
|           , _url: keyUrl | ||||
|           , _body: resp.body | ||||
|           } | ||||
|         }); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       var pubpem; | ||||
|       try { | ||||
|         pubpem = require('jwk-to-pem')(jwk, { private: false }); | ||||
|       } catch(e) { | ||||
|         pubpem = null; | ||||
|       } | ||||
| 			return verifyJwt(token, pubpem, { | ||||
| 				algorithms: [ 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512' ] | ||||
| 			}).then(function (decoded) { | ||||
|         if (!decoded) { | ||||
|           res.send({ | ||||
|             error: { | ||||
|               code: "E_UNVERIFIED" | ||||
|             , message: "retrieved jwk does not verify provided token." | ||||
|             , _jwk: jwk | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|         req.auth = {}; | ||||
|         req.auth.jwt = token; | ||||
|         req.auth.data = auth; | ||||
|         next(); | ||||
| 			}); | ||||
|     }); | ||||
|   }, function (err) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: err.code || "E_GENERIC" | ||||
|       , message: err.toString() | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| module.exports.pairRequest = function (opts) { | ||||
|   console.log("It's auth'n time!"); | ||||
|   var state = opts.state; | ||||
| @ -357,9 +598,36 @@ var urls = { | ||||
|   pairState: '/api/telebit.cloud/pair_state/:id' | ||||
| }; | ||||
| staticApp.use('/', express.static(path.join(__dirname, 'admin'))); | ||||
| app.use('/api', CORS({})); | ||||
| app.use('/api', CORS({ | ||||
|   credentials: true | ||||
| , headers: [ 'Authorization', 'X-Requested-With', 'X-HTTP-Method-Override', 'Content-Type', 'Accept' ] | ||||
| })); | ||||
| app.use('/api', bodyParser.json()); | ||||
| 
 | ||||
| app.use('/api/telebit.cloud/account', oauth3Auth); | ||||
| app.get('/api/telebit.cloud/account', function (req, res) { | ||||
|   Accounts.getBySub(req).then(function (subData) { | ||||
|     res.send(subData); | ||||
|   }, function (err) { | ||||
|     res.send({ | ||||
|       error: { | ||||
|         code: err.code || "E_GENERIC" | ||||
|       , message: err.toString() | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| app.post('/api/telebit.cloud/account', function (req, res) { | ||||
|   return Accounts.create(req).then(function (acc) { | ||||
|     res.send({ | ||||
|       success: true | ||||
|     , id: acc.id | ||||
|     , sub: acc.sub | ||||
|     , iss: acc.iss | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| // From Device (which knows id, but not secret)
 | ||||
| app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||
|   var auth = req.body; | ||||
| @ -383,7 +651,7 @@ app.post('/api/telebit.cloud/pair_request', function (req, res) { | ||||
| app.get('/api/telebit.cloud/pair_request/:secret', function (req, res) { | ||||
|   var secret = req.params.secret; | ||||
|   var auth = Auths.getBySecret(secret); | ||||
|   var crypto = require('crypto'); | ||||
|   //var crypto = require('crypto');
 | ||||
|   var response = {}; | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @ -37,8 +37,7 @@ | ||||
|   }, | ||||
|   "homepage": "https://git.coolaj86.com/coolaj86/telebit-relay.js", | ||||
|   "dependencies": { | ||||
|     "@coolaj86/urequest": "^1.1.1", | ||||
|     "bluebird": "^3.5.1", | ||||
|     "@coolaj86/urequest": "^1.2.1", | ||||
|     "body-parser": "^1.18.3", | ||||
|     "cluster-store": "^2.0.8", | ||||
|     "connect-cors": "^0.5.6", | ||||
| @ -48,16 +47,22 @@ | ||||
|     "greenlock": "^2.2.4", | ||||
|     "human-readable-ids": "^1.0.4", | ||||
|     "js-yaml": "^3.11.0", | ||||
|     "jsonwebtoken": "^8.2.1", | ||||
|     "jsonwebtoken": "^8.3.0", | ||||
|     "jwk-to-pem": "^2.0.0", | ||||
|     "mkdirp": "^0.5.1", | ||||
|     "nowww": "^1.2.1", | ||||
|     "proxy-packer": "^1.4.3", | ||||
|     "recase": "^1.0.4", | ||||
|     "redirect-https": "^1.1.5", | ||||
|     "request": "^2.87.0", | ||||
|     "safe-replace": "^1.0.3", | ||||
|     "serve-static": "^1.13.2", | ||||
|     "sni": "^1.0.0", | ||||
|     "ws": "^5.1.1" | ||||
|   }, | ||||
|   "trulyOptionalDependencies": { | ||||
|     "bluebird": "^3.5.1" | ||||
|   }, | ||||
|   "engineStrict": true, | ||||
|   "engines": { | ||||
|     "node": "10.2.1" | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user