demo email verification
This commit is contained in:
		
							parent
							
								
									155c006740
								
							
						
					
					
						commit
						87494faffe
					
				| @ -16,7 +16,7 @@ func main() { | ||||
| 	/* | ||||
| 	  MAILGUN_API_KEY=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||
| 	  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>'") | ||||
| @ -36,7 +36,7 @@ func main() { | ||||
| 
 | ||||
| 	domain := os.Getenv("MAILGUN_DOMAIN") | ||||
| 	apiKey := os.Getenv("MAILGUN_API_KEY") | ||||
| 	from := os.Getenv("MAILGUN_FROM") | ||||
| 	from := os.Getenv("MAILER_FROM") | ||||
| 
 | ||||
| 	if 0 == len(*text) { | ||||
| 		*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 ( | ||||
| 	git.rootprojects.org/root/keypairs v0.5.2 | ||||
| 	github.com/google/uuid v1.1.1 | ||||
| 	github.com/joho/godotenv v1.3.0 | ||||
| 	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/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/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/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= | ||||
| 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/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/base64" | ||||
| @ -15,14 +16,19 @@ import ( | ||||
| 	"math/big" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.rootprojects.org/root/keypairs" | ||||
| 	"git.rootprojects.org/root/keypairs/keyfetch" | ||||
| 
 | ||||
| 	//jwt "github.com/dgrijalva/jwt-go" | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| ) | ||||
| 
 | ||||
| type PublicJWK struct { | ||||
| @ -53,13 +59,39 @@ func (t *InspectableToken) MarshalJSON() ([]byte, error) { | ||||
| 	)), 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() { | ||||
| 	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) { | ||||
| 	// TODO get from main() | ||||
| 	tokPrefix := jwksPrefix | ||||
| 	pubkey := keypairs.NewPublicKey(privkey.Public()) | ||||
| 
 | ||||
| 	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.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) { | ||||
| @ -521,6 +630,7 @@ func JOSEVerify(pubkey keypairs.PublicKey, hash []byte, sig []byte) bool { | ||||
| 	case *rsa.PublicKey: | ||||
| 		// TODO keypairs.Size(key) to detect key size ? | ||||
| 		//alg := "SHA256" | ||||
| 		// TODO: this hasn't been tested yet | ||||
| 		if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash, sig); nil != err { | ||||
| 			verified = true | ||||
| 		} | ||||
| @ -566,7 +676,8 @@ func issueNonce(w http.ResponseWriter, r *http.Request) { | ||||
| 	b := make([]byte, 16) | ||||
| 	_, _ = rand.Read(b) | ||||
| 	nonce := base64.RawURLEncoding.EncodeToString(b) | ||||
| 	nonces[nonce] = time.Now().Unix() | ||||
| 	//nonCh <- nonce | ||||
| 	nonces.Store(nonce, time.Now()) | ||||
| 
 | ||||
| 	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) { | ||||
| 		nonce := r.Header.Get("Replay-Nonce") | ||||
| 		// TODO expire nonces every so often | ||||
| 		t := nonces[nonce] | ||||
| 		if 0 == t { | ||||
| 		//t := nonces[nonce] | ||||
| 		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( | ||||
| 				w, | ||||
| 				`{ "error": "invalid or expired nonce", "error_code": "ENONCE" }`, | ||||
| @ -585,7 +701,8 @@ func requireNonce(next http.HandlerFunc) http.HandlerFunc { | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		delete(nonces, nonce) | ||||
| 		//delete(nonces, nonce) | ||||
| 		nonces.Delete(nonce) | ||||
| 		issueNonce(w, r) | ||||
| 
 | ||||
| 		next(w, r) | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user