demo email verification
This commit is contained in:
		
							parent
							
								
									155c006740
								
							
						
					
					
						commit
						87494faffe
					
				| @ -16,7 +16,7 @@ func main() { | |||||||
| 	/* | 	/* | ||||||
| 	  MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | 	  MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
| 	  MAILGUN_DOMAIN=mail.example.com | 	  MAILGUN_DOMAIN=mail.example.com | ||||||
| 	  MAILGUN_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | 	  MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | ||||||
| 	*/ | 	*/ | ||||||
| 
 | 
 | ||||||
| 	to := flag.String("to", "", "message recipient in the format of 'John Doe <john@example.com>'") | 	to := flag.String("to", "", "message recipient in the format of 'John Doe <john@example.com>'") | ||||||
| @ -36,7 +36,7 @@ func main() { | |||||||
| 
 | 
 | ||||||
| 	domain := os.Getenv("MAILGUN_DOMAIN") | 	domain := os.Getenv("MAILGUN_DOMAIN") | ||||||
| 	apiKey := os.Getenv("MAILGUN_API_KEY") | 	apiKey := os.Getenv("MAILGUN_API_KEY") | ||||||
| 	from := os.Getenv("MAILGUN_FROM") | 	from := os.Getenv("MAILER_FROM") | ||||||
| 
 | 
 | ||||||
| 	if 0 == len(*text) { | 	if 0 == len(*text) { | ||||||
| 		*text = "Testing some Mailgun awesomeness!" | 		*text = "Testing some Mailgun awesomeness!" | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								examples/example.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								examples/example.env
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | SALT=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | ||||||
|  | 
 | ||||||
|  | MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | MAILGUN_DOMAIN=mail.example.com | ||||||
|  | 
 | ||||||
|  | MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | ||||||
|  | MAILER_REPLY_TO=support@example.com | ||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -4,6 +4,7 @@ go 1.13 | |||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	git.rootprojects.org/root/keypairs v0.5.2 | 	git.rootprojects.org/root/keypairs v0.5.2 | ||||||
|  | 	github.com/google/uuid v1.1.1 | ||||||
| 	github.com/joho/godotenv v1.3.0 | 	github.com/joho/godotenv v1.3.0 | ||||||
| 	github.com/mailgun/mailgun-go/v3 v3.6.4 | 	github.com/mailgun/mailgun-go/v3 v3.6.4 | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -10,6 +10,8 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQD | |||||||
| github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= | github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= | ||||||
| github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4= | github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4= | ||||||
| github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= | github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= | ||||||
|  | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= | ||||||
|  | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= | ||||||
| github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | ||||||
| github.com/mailgun/mailgun-go/v3 v3.6.4 h1:+cvbZRgLSHivbz/w1iWLmxVl6Bqf4geD2D7QMj4+8PE= | github.com/mailgun/mailgun-go/v3 v3.6.4 h1:+cvbZRgLSHivbz/w1iWLmxVl6Bqf4geD2D7QMj4+8PE= | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								mockid/mailgun.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								mockid/mailgun.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | package mockid | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	mailgun "github.com/mailgun/mailgun-go/v3" | ||||||
|  | 
 | ||||||
|  | 	_ "github.com/joho/godotenv/autoload" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	mgDomain string | ||||||
|  | 	mgAPIKey string | ||||||
|  | 	mgFrom   string | ||||||
|  | 	mg       *mailgun.MailgunImpl | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func init() { | ||||||
|  | 	/* | ||||||
|  | 	  MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||||
|  | 	  MAILGUN_DOMAIN=mail.example.com | ||||||
|  | 	  MAILER_FROM="Rob the Robot <rob.the.robot@mail.example.com>" | ||||||
|  | 	*/ | ||||||
|  | 
 | ||||||
|  | 	mgDomain = os.Getenv("MAILGUN_DOMAIN") | ||||||
|  | 	mgAPIKey = os.Getenv("MAILGUN_API_KEY") | ||||||
|  | 	mgFrom = os.Getenv("MAILER_FROM") | ||||||
|  | 
 | ||||||
|  | 	mg = mailgun.NewMailgun(mgDomain, mgAPIKey) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func SendSimpleMessage(to, from, subject, text, replyTo string) (string, error) { | ||||||
|  | 	m := mg.NewMessage(from, subject, text, to) | ||||||
|  | 	if 0 != len(replyTo) { | ||||||
|  | 		// mailgun's required "h:" prefix is added by the library | ||||||
|  | 		m.AddHeader("Reply-To", replyTo) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) | ||||||
|  | 	defer cancel() | ||||||
|  | 
 | ||||||
|  | 	_, id, err := mg.Send(ctx, m) | ||||||
|  | 	return id, err | ||||||
|  | } | ||||||
							
								
								
									
										131
									
								
								mockid/mockid.go
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								mockid/mockid.go
									
									
									
									
									
								
							| @ -5,6 +5,7 @@ import ( | |||||||
| 	"crypto/ecdsa" | 	"crypto/ecdsa" | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"crypto/rsa" | 	"crypto/rsa" | ||||||
|  | 	"crypto/sha1" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"crypto/sha512" | 	"crypto/sha512" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| @ -15,14 +16,19 @@ import ( | |||||||
| 	"math/big" | 	"math/big" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"git.rootprojects.org/root/keypairs" | 	"git.rootprojects.org/root/keypairs" | ||||||
| 	"git.rootprojects.org/root/keypairs/keyfetch" | 	"git.rootprojects.org/root/keypairs/keyfetch" | ||||||
|  | 
 | ||||||
| 	//jwt "github.com/dgrijalva/jwt-go" | 	//jwt "github.com/dgrijalva/jwt-go" | ||||||
|  | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type PublicJWK struct { | type PublicJWK struct { | ||||||
| @ -53,13 +59,39 @@ func (t *InspectableToken) MarshalJSON() ([]byte, error) { | |||||||
| 	)), nil | 	)), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var nonces map[string]int64 | var defaultFrom string | ||||||
|  | var defaultReplyTo string | ||||||
|  | 
 | ||||||
|  | //var nonces map[string]int64 | ||||||
|  | //var nonCh chan string | ||||||
|  | var nonces sync.Map | ||||||
|  | var salt []byte | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	nonces = make(map[string]int64) | 	var err error | ||||||
|  | 	salt64 := os.Getenv("SALT") | ||||||
|  | 	salt, err = base64.RawURLEncoding.DecodeString(salt64) | ||||||
|  | 	if len(salt64) < 22 || nil != err { | ||||||
|  | 		panic("SALT must be set as 22+ character base64") | ||||||
|  | 	} | ||||||
|  | 	defaultFrom = os.Getenv("MAILER_FROM") | ||||||
|  | 	defaultReplyTo = os.Getenv("MAILER_REPLY_TO") | ||||||
|  | 	//nonces = make(map[string]int64) | ||||||
|  | 	//nonCh = make(chan string) | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 		  go func() { | ||||||
|  | 		    for { | ||||||
|  | 		      nonce := <- nonCh | ||||||
|  | 			    nonces[nonce] = time.Now().Unix() | ||||||
|  | 		    } | ||||||
|  | 		  }() | ||||||
|  | 	*/ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Route(jwksPrefix string, privkey keypairs.PrivateKey) { | func Route(jwksPrefix string, privkey keypairs.PrivateKey) { | ||||||
|  | 	// TODO get from main() | ||||||
|  | 	tokPrefix := jwksPrefix | ||||||
| 	pubkey := keypairs.NewPublicKey(privkey.Public()) | 	pubkey := keypairs.NewPublicKey(privkey.Public()) | ||||||
| 
 | 
 | ||||||
| 	http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) { | 	http.HandleFunc("/api/new-nonce", func(w http.ResponseWriter, r *http.Request) { | ||||||
| @ -87,7 +119,84 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) { | |||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) { | 	http.HandleFunc("/api/new-account", requireNonce(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		http.Error(w, "Not Implemented", http.StatusNotImplemented) | 		// Try to decode the request body into the struct. If there is an error, | ||||||
|  | 		// respond to the client with the error message and a 400 status code. | ||||||
|  | 		data := map[string]string{} | ||||||
|  | 		err := json.NewDecoder(r.Body).Decode(&data) | ||||||
|  | 		if nil != err { | ||||||
|  | 			http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// TODO check DNS for MX records | ||||||
|  | 		parts := strings.Split(data["to"], ", <>\n\r\t") | ||||||
|  | 		to := parts[0] | ||||||
|  | 		if len(parts) > 1 || !strings.Contains(to, "@") { | ||||||
|  | 			http.Error(w, "invalid email address", http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		token, err := uuid.NewRandom() | ||||||
|  | 		if nil != err { | ||||||
|  | 			// nothing else to do if we run out of random | ||||||
|  | 			// or are on a platform that doesn't support random | ||||||
|  | 			panic(fmt.Errorf("random bytes read failure: %w", err)) | ||||||
|  | 		} | ||||||
|  | 		token64 := base64.RawURLEncoding.EncodeToString([]byte(token[:])) | ||||||
|  | 		// hash token to prevent fs read timing attacks | ||||||
|  | 		hash := sha1.Sum(append(token[:], salt...)) | ||||||
|  | 		tokname := base64.RawURLEncoding.EncodeToString(hash[:]) | ||||||
|  | 		if err := ioutil.WriteFile( | ||||||
|  | 			filepath.Join(tokPrefix, tokname+".tok.txt"), | ||||||
|  | 			[]byte(`{"comment":"I have no idea..."}`), | ||||||
|  | 			os.FileMode(0600), | ||||||
|  | 		); nil != err { | ||||||
|  | 			http.Error(w, "database connection failed when writing verification token", http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		subject := "Verify New Account" | ||||||
|  | 		// TODO go tpl | ||||||
|  | 		// TODO determine OS and Browser from user agent | ||||||
|  | 		baseURL := getBaseURL(r) | ||||||
|  | 		text := fmt.Sprintf( | ||||||
|  | 			"It looks like you just tried to register a new Pocket ID account.\n\n    Verify account: %s/verify/%s\n\nNot you? Just ignore this message.", | ||||||
|  | 			baseURL, token64, | ||||||
|  | 		) | ||||||
|  | 		_, err = SendSimpleMessage(to, defaultFrom, subject, text, defaultReplyTo) | ||||||
|  | 		if nil != err { | ||||||
|  | 			// TODO neuter mailgun output | ||||||
|  | 			http.Error(w, err.Error(), http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fmt.Fprintf(w, `{ "success": true, "error": "" }%s`, "\n") | ||||||
|  | 	})) | ||||||
|  | 
 | ||||||
|  | 	// TODO use chi | ||||||
|  | 	http.HandleFunc("/verify/", requireNonce(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		parts := strings.Split(r.URL.Path, "/") | ||||||
|  | 		if 3 != len(parts) { | ||||||
|  | 			http.Error(w, "invalid url path", http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		token64 := parts[2] | ||||||
|  | 		token, err := base64.RawURLEncoding.DecodeString(token64) | ||||||
|  | 		if err != nil || 0 == len(token) { | ||||||
|  | 			http.Error(w, "invalid url path", http.StatusBadRequest) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// hash token to prevent fs read timing attacks | ||||||
|  | 		hash := sha1.Sum(append(token, salt...)) | ||||||
|  | 		tokname := base64.RawURLEncoding.EncodeToString(hash[:]) | ||||||
|  | 		tokfile := filepath.Join(tokPrefix, tokname+".tok.txt") | ||||||
|  | 		_, err = ioutil.ReadFile(tokfile) | ||||||
|  | 		if nil != err { | ||||||
|  | 			http.Error(w, "database connection failed when reading verification token", http.StatusInternalServerError) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		os.Remove(tokfile) | ||||||
|  | 
 | ||||||
|  | 		fmt.Fprintf(w, `{ "success": true, "error": "" }%s`, "\n") | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
| 	http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) { | 	http.HandleFunc("/api/jwks", func(w http.ResponseWriter, r *http.Request) { | ||||||
| @ -521,6 +630,7 @@ func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool { | |||||||
| 	case *rsa.PublicKey: | 	case *rsa.PublicKey: | ||||||
| 		// TODO keypairs.Size(key) to detect key size ? | 		// TODO keypairs.Size(key) to detect key size ? | ||||||
| 		//alg := "SHA256" | 		//alg := "SHA256" | ||||||
|  | 		// TODO: this hasn't been tested yet | ||||||
| 		if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { | 		if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { | ||||||
| 			verified = true | 			verified = true | ||||||
| 		} | 		} | ||||||
| @ -566,7 +676,8 @@ func issueNonce(w http.ResponseWriter, r *http.Request) { | |||||||
| 	b := make([]byte, 16) | 	b := make([]byte, 16) | ||||||
| 	_, _ = rand.Read(b) | 	_, _ = rand.Read(b) | ||||||
| 	nonce := base64.RawURLEncoding.EncodeToString(b) | 	nonce := base64.RawURLEncoding.EncodeToString(b) | ||||||
| 	nonces[nonce] = time.Now().Unix() | 	//nonCh <- nonce | ||||||
|  | 	nonces.Store(nonce, time.Now()) | ||||||
| 
 | 
 | ||||||
| 	w.Header().Set("Replay-Nonce", nonce) | 	w.Header().Set("Replay-Nonce", nonce) | ||||||
| } | } | ||||||
| @ -575,8 +686,13 @@ func requireNonce(next http.HandlerFunc) http.HandlerFunc { | |||||||
| 	return func(w http.ResponseWriter, r *http.Request) { | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		nonce := r.Header.Get("Replay-Nonce") | 		nonce := r.Header.Get("Replay-Nonce") | ||||||
| 		// TODO expire nonces every so often | 		// TODO expire nonces every so often | ||||||
| 		t := nonces[nonce] | 		//t := nonces[nonce] | ||||||
| 		if 0 == t { | 		var t time.Time | ||||||
|  | 		tmp, ok := nonces.Load(nonce) | ||||||
|  | 		if ok { | ||||||
|  | 			t = tmp.(time.Time) | ||||||
|  | 		} | ||||||
|  | 		if !ok || time.Now().Sub(t) > 15*time.Minute { | ||||||
| 			http.Error( | 			http.Error( | ||||||
| 				w, | 				w, | ||||||
| 				`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`, | 				`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`, | ||||||
| @ -585,7 +701,8 @@ func requireNonce(next http.HandlerFunc) http.HandlerFunc { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		delete(nonces, nonce) | 		//delete(nonces, nonce) | ||||||
|  | 		nonces.Delete(nonce) | ||||||
| 		issueNonce(w, r) | 		issueNonce(w, r) | ||||||
| 
 | 
 | ||||||
| 		next(w, r) | 		next(w, r) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user