update errors and iframe
This commit is contained in:
		
							parent
							
								
									881bf97334
								
							
						
					
					
						commit
						84e1863da2
					
				| @ -11,6 +11,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"log" | 	"log" | ||||||
|  | 	"net" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| @ -28,8 +29,30 @@ import ( | |||||||
| 	ua "github.com/mileusna/useragent" | 	ua "github.com/mileusna/useragent" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | var errTokenNotVerified = apiError{"token has not been verified"} | ||||||
|  | var errUsedToken = apiError{"token has already been used"} | ||||||
|  | var errInvalidEmail = apiError{"invalid email address"} | ||||||
|  | 
 | ||||||
|  | // API Errors | ||||||
|  | type serverError struct { | ||||||
|  | 	error string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (e serverError) Error() string { | ||||||
|  | 	return e.error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type apiError struct { | ||||||
|  | 	error string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (e apiError) Error() string { | ||||||
|  | 	return e.error | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type HTTPResponse struct { | type HTTPResponse struct { | ||||||
| 	Error   string `json:"error"` | 	Error   string `json:"error,omitempty"` | ||||||
|  | 	Code    string `json:"code,omitempty"` | ||||||
| 	Success bool   `json:"success"` | 	Success bool   `json:"success"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -135,13 +158,22 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		contact, err = lintEmail(contact) | ||||||
|  | 		if nil != err { | ||||||
|  | 			b, _ := json.Marshal(&HTTPResponse{ | ||||||
|  | 				Error: err.Error(), | ||||||
|  | 				Code:  "E_USER", | ||||||
|  | 			}) | ||||||
|  | 			w.Write(b) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		_, ok, err := contactKV.Load(contact) | 		_, ok, err := contactKV.Load(contact) | ||||||
| 		if nil != err { | 		if nil != err { | ||||||
| 			fmt.Fprintf(os.Stderr, "meta: error loading contact: %s\n", err.Error()) | 			fmt.Fprintf(os.Stderr, "meta: error loading contact: %s\n", err.Error()) | ||||||
| 			http.Error(w, "Internal Server Error", http.StatusInternalServerError) | 			http.Error(w, "Internal Server Error", http.StatusInternalServerError) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		if ok { | 		if ok { | ||||||
| 			b, _ := json.Marshal(&HTTPResponse{ | 			b, _ := json.Marshal(&HTTPResponse{ | ||||||
| 				Success: true, | 				Success: true, | ||||||
| @ -152,6 +184,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { | |||||||
| 
 | 
 | ||||||
| 		b, _ := json.Marshal(&HTTPResponse{ | 		b, _ := json.Marshal(&HTTPResponse{ | ||||||
| 			Error: "not found", | 			Error: "not found", | ||||||
|  | 			Code:  "E_USER", | ||||||
| 		}) | 		}) | ||||||
| 		w.Write(b) | 		w.Write(b) | ||||||
| 	}) | 	}) | ||||||
| @ -217,6 +250,13 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { | |||||||
| 			otp, err = consumeOTPReceipt("TODO", receipt, agent, addr) | 			otp, err = consumeOTPReceipt("TODO", receipt, agent, addr) | ||||||
| 		} | 		} | ||||||
| 		if nil != err { | 		if nil != err { | ||||||
|  | 			if errTokenNotVerified == err { | ||||||
|  | 				b, _ := json.Marshal(&HTTPResponse{ | ||||||
|  | 					Error: err.Error(), | ||||||
|  | 				}) | ||||||
|  | 				w.Write(b) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 			// TODO propagate error types | 			// TODO propagate error types | ||||||
| 			http.Error(w, "Bad Request", http.StatusBadRequest) | 			http.Error(w, "Bad Request", http.StatusBadRequest) | ||||||
| 			fmt.Fprintf(w, "%s", err) | 			fmt.Fprintf(w, "%s", err) | ||||||
| @ -821,7 +861,7 @@ func newOTP(email string, agent string, addr string) (id string, secret []byte, | |||||||
| 		// keep it secret, keep it safe | 		// keep it secret, keep it safe | ||||||
| 		os.FileMode(0600), | 		os.FileMode(0600), | ||||||
| 	); nil != err { | 	); nil != err { | ||||||
| 		return "", nil, errors.New("database connection failed when writing verification token") | 		return "", nil, serverError{"database connection failed when writing verification token"} | ||||||
| 	} | 	} | ||||||
| 	return receipt, secret, nil | 	return receipt, secret, nil | ||||||
| } | } | ||||||
| @ -847,12 +887,12 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error | |||||||
| 	tokfile := filepath.Join(tokenPrefix, hash+".tok.txt") | 	tokfile := filepath.Join(tokenPrefix, hash+".tok.txt") | ||||||
| 	b, err := ioutil.ReadFile(tokfile) | 	b, err := ioutil.ReadFile(tokfile) | ||||||
| 	if nil != err { | 	if nil != err { | ||||||
| 		return nil, errors.New("database connection failed when reading verification token") | 		return nil, serverError{"database connection failed when reading verification token"} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	otp := OTP{} | 	otp := OTP{} | ||||||
| 	if err := json.Unmarshal(b, &otp); nil != err { | 	if err := json.Unmarshal(b, &otp); nil != err { | ||||||
| 		return nil, errors.New("database verification token parse failed") | 		return nil, serverError{"database verification token parse failed"} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if 0 == subtle.ConstantTimeCompare([]byte(otp.Email), []byte(email)) { | 	if 0 == subtle.ConstantTimeCompare([]byte(otp.Email), []byte(email)) { | ||||||
| @ -862,7 +902,7 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error | |||||||
| 
 | 
 | ||||||
| 	if consume.secret { | 	if consume.secret { | ||||||
| 		if !otp.SecretUsed.IsZero() { | 		if !otp.SecretUsed.IsZero() { | ||||||
| 			return nil, errors.New("token has already been used") | 			return nil, errUsedToken | ||||||
| 		} | 		} | ||||||
| 		otp.SecretUsed = time.Now() | 		otp.SecretUsed = time.Now() | ||||||
| 		if addr != otp.ReceiptIP { | 		if addr != otp.ReceiptIP { | ||||||
| @ -874,10 +914,10 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error | |||||||
| 		} | 		} | ||||||
| 	} else if consume.receipt { | 	} else if consume.receipt { | ||||||
| 		if otp.SecretUsed.IsZero() { | 		if otp.SecretUsed.IsZero() { | ||||||
| 			return nil, errors.New("token has not been verified") | 			return nil, errTokenNotVerified | ||||||
| 		} | 		} | ||||||
| 		if !otp.ReceiptUsed.IsZero() { | 		if !otp.ReceiptUsed.IsZero() { | ||||||
| 			return nil, errors.New("token has already been used") | 			return nil, errUsedToken | ||||||
| 		} | 		} | ||||||
| 		otp.ReceiptUsed = time.Now() | 		otp.ReceiptUsed = time.Now() | ||||||
| 	} | 	} | ||||||
| @ -890,24 +930,38 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error | |||||||
| 			// keep it secret, keep it safe | 			// keep it secret, keep it safe | ||||||
| 			os.FileMode(0600), | 			os.FileMode(0600), | ||||||
| 		); nil != err { | 		); nil != err { | ||||||
| 			return nil, errors.New("database connection failed when consuming token") | 			return nil, serverError{"database connection failed when consuming token"} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	fmt.Println("THE TOKEN IS GOOD. GOOD!!") |  | ||||||
| 
 |  | ||||||
| 	return &otp, nil | 	return &otp, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func lintEmail(email string) (string, error) { | ||||||
|  | 	// TODO check DNS for MX records | ||||||
|  | 	parts := strings.Split(email, "@") | ||||||
|  | 	domain := parts[1] | ||||||
|  | 	if 2 != len(parts) || strings.Contains(email, " \t\n") { | ||||||
|  | 		return "", errInvalidEmail | ||||||
|  | 	} | ||||||
|  | 	mxs, err := net.LookupMX(domain) | ||||||
|  | 	if len(mxs) < 1 || nil != err { | ||||||
|  | 		// TODO it possible in some cases that this | ||||||
|  | 		// could be a network error | ||||||
|  | 		return "", errInvalidEmail | ||||||
|  | 	} | ||||||
|  | 	return strings.ToLower(email), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func startVerification(baseURL, contact, agent, addr string) (receipt string, err error) { | func startVerification(baseURL, contact, agent, addr string) (receipt string, err error) { | ||||||
| 	email := strings.Replace(strings.TrimPrefix(contact, "mailto:"), " ", "+", -1) | 	email := strings.Replace(strings.TrimPrefix(contact, "mailto:"), " ", "+", -1) | ||||||
| 	if "" == email { | 	if "" == email { | ||||||
| 		return "", errors.New("missing contact:[\"mailto:me@example.com\"]") | 		return "", errors.New("missing contact:[\"mailto:me@example.com\"]") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// TODO check DNS for MX records | 	email, err = lintEmail(email) | ||||||
| 	if !strings.Contains(email, "@") || strings.Contains(email, " \t\n") { | 	if nil != err { | ||||||
| 		return "", errors.New("invalid email address") | 		return "", err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// TODO expect JWK in JWS/JWT | 	// TODO expect JWK in JWS/JWT | ||||||
| @ -922,9 +976,10 @@ func startVerification(baseURL, contact, agent, addr string) (receipt string, er | |||||||
| 	subject := "Verify New Account" | 	subject := "Verify New Account" | ||||||
| 	// TODO go tpl | 	// TODO go tpl | ||||||
| 	// TODO determine OS and Browser from user agent | 	// TODO determine OS and Browser from user agent | ||||||
|  | 	page := "pocket/iframe.html" | ||||||
| 	text := fmt.Sprintf( | 	text := fmt.Sprintf( | ||||||
| 		"It looks like you just tried to register a new Pocket ID account.\n\n    Verify account: %s#/verify/%s\n\n%s on %s %s from %s\n\nNot you? Just ignore this message.", | 		"It looks like you just tried to register a new Pocket ID account.\n\n    Verify account: %s/%s#/verify/%s\n\n%s on %s %s from %s\n\nNot you? Just ignore this message.", | ||||||
| 		baseURL, base64.RawURLEncoding.EncodeToString(secret), ua.Name, ua.OS, ua.Device, addr, | 		baseURL, page, base64.RawURLEncoding.EncodeToString(secret), ua.Name, ua.OS, ua.Device, addr, | ||||||
| 	) | 	) | ||||||
| 	fmt.Println("email:", text) | 	fmt.Println("email:", text) | ||||||
| 	if !strings.Contains(contact, "+noreply") { | 	if !strings.Contains(contact, "+noreply") { | ||||||
|  | |||||||
| @ -1,79 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| /*global crypto*/ |  | ||||||
| 
 |  | ||||||
| var Hashcash = module.exports; |  | ||||||
| 
 |  | ||||||
| var textEncoder = new TextEncoder(); |  | ||||||
| Hashcash.solve = async function solveHc(hc) { |  | ||||||
| 	var solution = 0; |  | ||||||
| 	var parts = hc.split(':').slice(0, 6); |  | ||||||
| 	if (parts.length < 6) { |  | ||||||
| 		throw new Error('invalid Hashcash-Challenge: ' + hc); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var bits = parseInt(parts[1], 10) || -1; |  | ||||||
| 	if (bits > 10 || bits < 0) { |  | ||||||
| 		throw new Error('bad bit values'); |  | ||||||
| 	} |  | ||||||
| 	console.log('bits:', bits); |  | ||||||
| 	hc = parts.join(':') + ':'; |  | ||||||
| 	async function next() { |  | ||||||
| 		var answer = hc + int52ToBase64(solution); |  | ||||||
| 		var u8 = textEncoder.encode(answer); |  | ||||||
| 		// REALLY SLOW due to async tasks and C++ context switch
 |  | ||||||
| 		var hash = await crypto.subtle.digest('SHA-256', u8); |  | ||||||
| 		hash = new Uint8Array(hash); |  | ||||||
| 		if (checkHc(hash, bits)) { |  | ||||||
| 			return answer; |  | ||||||
| 		} |  | ||||||
| 		solution += 1; |  | ||||||
| 		return next(); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return next(); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function int52ToBase64(n) { |  | ||||||
| 	var hex = n.toString(16); |  | ||||||
| 	if (hex.length % 2) { |  | ||||||
| 		hex = '0' + hex; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var bin = []; |  | ||||||
| 	var i = 0; |  | ||||||
| 	var d; |  | ||||||
| 	var b; |  | ||||||
| 	while (i < hex.length) { |  | ||||||
| 		d = parseInt(hex.slice(i, i + 2), 16); |  | ||||||
| 		b = String.fromCharCode(d); |  | ||||||
| 		bin.push(b); |  | ||||||
| 		i += 2; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return btoa(bin.join('')).replace(/=/g, ''); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function checkHc(hash, bits) { |  | ||||||
| 	var n = Math.floor(bits / 8); |  | ||||||
| 	var m = bits % 8; |  | ||||||
| 	var i; |  | ||||||
| 	if (m > 0) { |  | ||||||
| 		n += 1; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for (i = 0; i < n && i < hash.length; i += 1) { |  | ||||||
| 		if (bits > 8) { |  | ||||||
| 			bits -= 8; |  | ||||||
| 			if (0 !== hash[i]) { |  | ||||||
| 				return false; |  | ||||||
| 			} |  | ||||||
| 			continue; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if (0 !== hash[i] >> (8 - bits)) { |  | ||||||
| 			return false; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return true; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										213
									
								
								public/main.js
									
									
									
									
									
								
							
							
						
						
									
										213
									
								
								public/main.js
									
									
									
									
									
								
							| @ -1,214 +1,3 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var request = require('./request.js'); | require('./pocket/consumer.js'); | ||||||
| var PocketId = require('./pocketid.js'); |  | ||||||
| var state = {}; |  | ||||||
| var auths = clearAuths(); |  | ||||||
| 
 |  | ||||||
| function $$(sel, el) { |  | ||||||
| 	if (el) { |  | ||||||
| 		return el.querySelectorAll(sel) || []; |  | ||||||
| 	} |  | ||||||
| 	return document.body.querySelectorAll(sel) || []; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function $(sel, el) { |  | ||||||
| 	if (el) { |  | ||||||
| 		return el.querySelector(sel); |  | ||||||
| 	} |  | ||||||
| 	return document.body.querySelector(sel); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function clearAuths() { |  | ||||||
| 	var _auths = { |  | ||||||
| 		google: { |  | ||||||
| 			promise: null, |  | ||||||
| 			idToken: '' |  | ||||||
| 		} |  | ||||||
| 	}; |  | ||||||
| 	_auths.google.promise = new Promise(function (res, rej) { |  | ||||||
| 		_auths.google.resolve = res; |  | ||||||
| 		_auths.google.reject = rej; |  | ||||||
| 	}); |  | ||||||
| 	return _auths; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| window.onSignIn = async function onSignIn(googleUser) { |  | ||||||
| 	// Useful data for your client-side scripts:
 |  | ||||||
| 	var profile = googleUser.getBasicProfile(); |  | ||||||
| 	// Don't send this directly to your server!
 |  | ||||||
| 	console.log('ID: ' + profile.getId()); |  | ||||||
| 	console.log('Full Name: ' + profile.getName()); |  | ||||||
| 	console.log('Given Name: ' + profile.getGivenName()); |  | ||||||
| 	console.log('Family Name: ' + profile.getFamilyName()); |  | ||||||
| 	console.log('Image URL: ' + profile.getImageUrl()); |  | ||||||
| 	console.log('Email: ' + profile.getEmail()); |  | ||||||
| 
 |  | ||||||
| 	// The ID token you need to pass to your backend:
 |  | ||||||
| 	auths.google.idToken = googleUser.getAuthResponse().id_token; |  | ||||||
| 	console.log('ID Token: ' + auths.google.idToken); |  | ||||||
| 	auths.google.resolve(auths.google.idToken); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function setFlow(cont, flow) { |  | ||||||
| 	$$(cont).forEach(function (el) { |  | ||||||
| 		el.hidden = true; |  | ||||||
| 	}); |  | ||||||
| 	console.log(flow); |  | ||||||
| 	$(flow).hidden = false; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| async function unlock() { |  | ||||||
| 	var key; |  | ||||||
| 	try { |  | ||||||
| 		key = await PocketId.unlock(function () { |  | ||||||
| 			setFlow('.authn-container', '.authn-unlock'); |  | ||||||
| 			return new Promise(function (resolve, reject) { |  | ||||||
| 				window.unlocker = { resolve: resolve, reject: reject }; |  | ||||||
| 			}); |  | ||||||
| 		}); |  | ||||||
| 	} catch (e) { |  | ||||||
| 		console.error( |  | ||||||
| 			"Had a key, but couldn't unlock it. TODO: Just send email?" |  | ||||||
| 		); |  | ||||||
| 		console.error(e); |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	setFlow('.authn-container', '.authn-loading'); |  | ||||||
| 
 |  | ||||||
| 	if (key) { |  | ||||||
| 		genTokenWithKey(key); |  | ||||||
| 		return; |  | ||||||
| 		await PocketId.createIdToken({ key: key }); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	PocketId.signIdToken(id_token).then(function (resp) { |  | ||||||
| 		console.log('Response:'); |  | ||||||
| 		console.log(resp); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function genTokenWithKey() { |  | ||||||
| 	// TODO: generate token
 |  | ||||||
| 	// TODO: check if the key is still considered valid
 |  | ||||||
| 	// TODO: generate new key and authorize
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| (async function () { |  | ||||||
| 	var loc = window.location; |  | ||||||
| 
 |  | ||||||
| 	console.log('/new-hashcash?'); |  | ||||||
| 	var resp = await request({ |  | ||||||
| 		method: 'POST', |  | ||||||
| 		url: loc.protocol + '//' + loc.hostname + '/api/new-hashcash' |  | ||||||
| 	}); |  | ||||||
| 	console.log(resp); |  | ||||||
| 
 |  | ||||||
| 	console.log('/test-hashcash?'); |  | ||||||
| 	resp = await request({ |  | ||||||
| 		method: 'POST', |  | ||||||
| 		url: loc.protocol + '//' + loc.hostname + '/api/test-hashcash' |  | ||||||
| 	}); |  | ||||||
| 	console.log(resp); |  | ||||||
| })(); |  | ||||||
| 
 |  | ||||||
| setFlow('.authn-container', '.authn-email'); |  | ||||||
| 
 |  | ||||||
| $('.authn-email form').addEventListener('submit', function (ev) { |  | ||||||
| 	ev.preventDefault(); |  | ||||||
| 	ev.stopPropagation(); |  | ||||||
| 	state.email = $('.authn-email [name=username]').value; |  | ||||||
| 
 |  | ||||||
| 	setFlow('.authn-container', '.authn-loading'); |  | ||||||
| 	return PocketId.auth |  | ||||||
| 		.meta({ email: state.email }) |  | ||||||
| 		.catch(function (err) { |  | ||||||
| 			window.alert('Error: ' + err.message); |  | ||||||
| 		}) |  | ||||||
| 		.then(function (resp) { |  | ||||||
| 			// if the user exists, go to the continue screen
 |  | ||||||
| 			// otherwise go to the new user screen
 |  | ||||||
| 			console.log('meta:', resp); |  | ||||||
| 			if (!resp.body.success) { |  | ||||||
| 				// This is a completely new user
 |  | ||||||
| 				setFlow('.authn-container', '.authn-new-user'); |  | ||||||
| 				return; |  | ||||||
| 			} |  | ||||||
| 			// The user exists, but this is a new device
 |  | ||||||
| 			setFlow('.authn-container', '.authn-existing'); |  | ||||||
| 		}); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| function verifyNewDevice() { |  | ||||||
| 	return PocketId.auth |  | ||||||
| 		.verify({ scheme: 'mailto:', email: state.email }) |  | ||||||
| 		.catch(function (err) { |  | ||||||
| 			window.alert('Error: ' + err.message); |  | ||||||
| 		}) |  | ||||||
| 		.then(function (resp) { |  | ||||||
| 			console.log(resp); |  | ||||||
| 			localStorage.setItem( |  | ||||||
| 				'pocketid', // + state.email,
 |  | ||||||
| 				JSON.stringify({ |  | ||||||
| 					receipt: resp.body.receipt, |  | ||||||
| 					email: state.email, |  | ||||||
| 					createdAt: new Date().toISOString() |  | ||||||
| 				}) |  | ||||||
| 			); |  | ||||||
| 			window.alert("Go check yo' email!"); |  | ||||||
| 			return PocketId.auth |  | ||||||
| 				.consume({ |  | ||||||
| 					email: state.email, |  | ||||||
| 					receipt: resp.body.receipt |  | ||||||
| 				}) |  | ||||||
| 				.then(function (resp) { |  | ||||||
| 					// this should have a token we can inspect
 |  | ||||||
| 					// and return to the calling application.
 |  | ||||||
| 					console.log(resp); |  | ||||||
| 					window.alert('all set!'); |  | ||||||
| 				}); |  | ||||||
| 		}); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| $('.authn-existing form').addEventListener('submit', function (ev) { |  | ||||||
| 	ev.preventDefault(); |  | ||||||
| 	ev.stopPropagation(); |  | ||||||
| 
 |  | ||||||
| 	setFlow('.authn-container', '.authn-loading'); |  | ||||||
| 	verifyNewDevice(); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| $('.authn-new-user form').addEventListener('submit', function (ev) { |  | ||||||
| 	ev.preventDefault(); |  | ||||||
| 	ev.stopPropagation(); |  | ||||||
| 
 |  | ||||||
| 	// We don't need to worry about checking if the key exists
 |  | ||||||
| 	// even if it does, the account has been deactivated
 |  | ||||||
| 
 |  | ||||||
| 	setFlow('.authn-container', '.authn-loading'); |  | ||||||
| 	verifyNewDevice(); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| var route = window.location.hash.split('/').slice(1); |  | ||||||
| console.log('route:', route); |  | ||||||
| switch (route[0]) { |  | ||||||
| 	case 'verify': |  | ||||||
| 		var pstate = JSON.parse(localStorage.getItem('pocketid') || '{}'); |  | ||||||
| 		PocketId.auth |  | ||||||
| 			.consume({ |  | ||||||
| 				receipt: pstate.receipt, |  | ||||||
| 				secret: route[1] |  | ||||||
| 			}) |  | ||||||
| 			.then(function (resp) { |  | ||||||
| 				console.log('token for this device to save:', resp); |  | ||||||
| 				window.alert('goodness!'); |  | ||||||
| 			}) |  | ||||||
| 			.catch(function (e) { |  | ||||||
| 				console.error(e); |  | ||||||
| 				window.alert('network error, try again'); |  | ||||||
| 			}); |  | ||||||
| 		break; |  | ||||||
| 	default: |  | ||||||
| 	// do nothing
 |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -1,118 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var Keypairs = require('@root/keypairs'); |  | ||||||
| var PocketId = module.exports; |  | ||||||
| var request = require('./request.js'); |  | ||||||
| 
 |  | ||||||
| var keyJson = window.localStorage.getItem('private.jwk.json'); |  | ||||||
| 
 |  | ||||||
| PocketId.signIdToken = async function (idToken) { |  | ||||||
| 	var pair = await Keypairs.parseOrGenerate({ key: keyJson }); |  | ||||||
| 	var jwt = await Keypairs.signJwt({ |  | ||||||
| 		jwk: pair.private, |  | ||||||
| 		iss: window.location.protocol + '//' + window.location.hostname, |  | ||||||
| 		exp: '15m', |  | ||||||
| 		claims: { |  | ||||||
| 			contact: ['google:' + idToken] |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
| 	return jwt; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| PocketId.auth = {}; |  | ||||||
| PocketId.auth.meta = async function ({ email }) { |  | ||||||
| 	var loc = window.location; |  | ||||||
| 	var body = await request({ |  | ||||||
| 		method: 'GET', |  | ||||||
| 		url: |  | ||||||
| 			loc.protocol + |  | ||||||
| 			'//' + |  | ||||||
| 			loc.hostname + |  | ||||||
| 			'/api/authn/meta?contact=' + |  | ||||||
| 			'mailto:' + |  | ||||||
| 			email |  | ||||||
| 	}); |  | ||||||
| 	return body; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| PocketId.auth.verify = async function ({ scheme, email }) { |  | ||||||
| 	if (!scheme) { |  | ||||||
| 		scheme = 'mailto:'; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var loc = window.location; |  | ||||||
| 	var body = await request({ |  | ||||||
| 		method: 'GET', |  | ||||||
| 		url: |  | ||||||
| 			loc.protocol + |  | ||||||
| 			'//' + |  | ||||||
| 			loc.hostname + |  | ||||||
| 			'/api/authn/verify?contact=' + |  | ||||||
| 			scheme + |  | ||||||
| 			email |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	return body; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| PocketId.auth.consume = async function ({ |  | ||||||
| 	email = '', |  | ||||||
| 	receipt = '', |  | ||||||
| 	secret = '', |  | ||||||
| 	count = 0 |  | ||||||
| }) { |  | ||||||
| 	var loc = window.location; |  | ||||||
| 	var resp = await request({ |  | ||||||
| 		method: 'GET', |  | ||||||
| 		url: |  | ||||||
| 			loc.protocol + |  | ||||||
| 			'//' + |  | ||||||
| 			loc.hostname + |  | ||||||
| 			'/api/authn/consume?contact=' + |  | ||||||
| 			(email ? 'mailto:' + email : '') + |  | ||||||
| 			'&receipt=' + |  | ||||||
| 			receipt + |  | ||||||
| 			'&secret=' + |  | ||||||
| 			secret |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	if (resp.body.success) { |  | ||||||
| 		// There should be a token here
 |  | ||||||
| 		// (or the pubkey should have been given beforehand)
 |  | ||||||
| 		return resp.body; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if (resp.body.error) { |  | ||||||
| 		// TODO special errors are hard failures
 |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if (count > 600) { |  | ||||||
| 		throw new Error('abandoned login'); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return timeout(5000).then(function () { |  | ||||||
| 		console.log('check otp again'); |  | ||||||
| 		return PocketId.auth.consume({ |  | ||||||
| 			email, |  | ||||||
| 			secret, |  | ||||||
| 			receipt, |  | ||||||
| 			count: count || 0 |  | ||||||
| 		}); |  | ||||||
| 	}); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| async function timeout(ms) { |  | ||||||
| 	return new Promise(function (resolve) { |  | ||||||
| 		setTimeout(resolve, ms); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var textEncoder = new TextEncoder(); |  | ||||||
| PocketId.genKey = async function ({ email }) { |  | ||||||
| 	// Ideally we'd use PBKDF2 or better but... web standards...
 |  | ||||||
| 	// TODO put a random salt
 |  | ||||||
| 	var emailU8 = textEncoder.encode(email); |  | ||||||
| 	var salt = await crypto.subtle.digest('SHA-256', emailU8); |  | ||||||
| 	var u8 = textEncoder.encode(answer); |  | ||||||
| 	var hash = await crypto.subtle.digest('SHA-256', u8); |  | ||||||
| }; |  | ||||||
| @ -1,85 +0,0 @@ | |||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var Hashcash = require('./hashcash.js'); |  | ||||||
| var _sites = {}; |  | ||||||
| 
 |  | ||||||
| module.exports = async function (opts) { |  | ||||||
| 	if (!opts.headers) { |  | ||||||
| 		opts.headers = {}; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if (opts.json) { |  | ||||||
| 		if (true === opts.json) { |  | ||||||
| 			opts.body = JSON.stringify(opts.body); |  | ||||||
| 		} else { |  | ||||||
| 			opts.body = JSON.stringify(opts.json); |  | ||||||
| 		} |  | ||||||
| 		if (!opts.headers['Content-Type'] && !opts.headers['content-type']) { |  | ||||||
| 			opts.headers['Content-Type'] = 'application/json'; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if (!opts.mode) { |  | ||||||
| 		opts.mode = 'cors'; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var url = new URL(opts.url); |  | ||||||
| 	if (!_sites[url.hostname]) { |  | ||||||
| 		_sites[url.hostname] = { nonces: [] }; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var site = _sites[url.hostname]; |  | ||||||
| 	var hc = site.hashcashChallenge; |  | ||||||
| 	if (hc) { |  | ||||||
| 		delete site.hashcashChallenge; |  | ||||||
| 		site.hashcash = await Hashcash.solve(hc); |  | ||||||
| 	} |  | ||||||
| 	if (site.hashcash) { |  | ||||||
| 		opts.headers.Hashcash = site.hashcash; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var response = await window.fetch(opts.url, opts); |  | ||||||
| 	var headerNames = response.headers.keys(); |  | ||||||
| 	var hs = {}; |  | ||||||
| 	var h; |  | ||||||
| 	while (true) { |  | ||||||
| 		h = headerNames.next(); |  | ||||||
| 		if (h.done) { |  | ||||||
| 			break; |  | ||||||
| 		} |  | ||||||
| 		hs[h.value] = response.headers.get(h.value); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var body; |  | ||||||
| 	if (hs['content-type'].includes('application/json')) { |  | ||||||
| 		body = await response.json(); |  | ||||||
| 	} else { |  | ||||||
| 		body = await response.text(); |  | ||||||
| 		try { |  | ||||||
| 			body = JSON.parse(body); |  | ||||||
| 		} catch (e) { |  | ||||||
| 			// ignore
 |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	var resp = {}; |  | ||||||
| 
 |  | ||||||
| 	resp.body = body; |  | ||||||
| 	resp.headers = hs; |  | ||||||
| 	resp.toJSON = function () { |  | ||||||
| 		return { |  | ||||||
| 			headers: hs, |  | ||||||
| 			body: body |  | ||||||
| 		}; |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	if (resp.headers['hashcash-challenge']) { |  | ||||||
| 		_sites[url.hostname].hashcashChallenge = |  | ||||||
| 			resp.headers['hashcash-challenge']; |  | ||||||
| 	} |  | ||||||
| 	if (resp.headers.nonce) { |  | ||||||
| 		site.nonces.push(resp.headers.nonce); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
|   console.log(resp); |  | ||||||
| 	return resp; |  | ||||||
| }; |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user