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…
Reference in New Issue