package main import ( "crypto/rand" "crypto/rsa" "database/sql" "encoding/hex" "encoding/json" "fmt" "io" "io/ioutil" "log" "math/big" "net/http" "net/url" "os" "strings" "time" "github.com/dgrijalva/jwt-go" "github.com/go-redis/redis" "github.com/joho/godotenv" "github.com/julienschmidt/httprouter" _ "github.com/lib/pq" "github.com/ttacon/libphonenumber" ) var listen string var postgres string var redisHost string var ttl time.Duration var messagingSID string var privateKey *rsa.PrivateKey var dummyToken string var coreURL string var twilioSID string var twilioToken string var db *sql.DB var redisClient *redis.Client func main() { // Load .env err := godotenv.Load() if err != nil { log.Fatal("Error loading .env file") } listen = os.Getenv("LISTEN") postgres = os.Getenv("POSTGRES") redisHost = os.Getenv("REDIS") ttl, err = time.ParseDuration(os.Getenv("TTL")) if err != nil { log.Fatal("Error parsing ttl") } messagingSID = os.Getenv("MESSAGING_SID") twilioSID = os.Getenv("TWILIO_SID") twilioToken = os.Getenv("TWILIO_TOKEN") dummyToken = "{\"userid\":\"dummy\",\"clientid\":\"dummy\"}" coreURL = os.Getenv("CORE_URL") // Load RSA private key privateKeyBytes, err := ioutil.ReadFile("key") if err != nil { log.Fatal(err) } privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes) if err != nil { log.Fatal(err) } // Postgres log.Printf("connecting to postgres %s", postgres) db, err = sql.Open("postgres", postgres) if err != nil { log.Fatal(err) } defer db.Close() // Redis redisClient = redis.NewClient(&redis.Options{ Addr: redisHost, Password: "", DB: 1, }) // Routes router := httprouter.New() router.POST("/init", InitRequest) router.POST("/init/bypass", InitRequestBypass) router.POST("/verify", VerifyCode) router.POST("/register/:code/:nonce", CreateUser) // Start server log.Printf("starting server on %s", listen) log.Fatal(http.ListenAndServe(listen, router)) } func ParsePhone(phone string) (string, error) { num, err := libphonenumber.Parse(phone, "") if err != nil { return "", err } return libphonenumber.Format(num, libphonenumber.INTERNATIONAL), nil } func RandomHex() (string, error) { b := make([]byte, 16) _, err := rand.Read(b) return hex.EncodeToString(b), err } type InitRequestBody struct { PhoneNumber string `json:"phone_number"` } func InitRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) { // Get request body req := InitRequestBody{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&req) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // Make sure phone number is legitimate phone, err := ParsePhone(req.PhoneNumber) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // Generate OTP code c, err := rand.Int(rand.Reader, big.NewInt(1000000)) code := fmt.Sprintf("%06d", c) // Generate nonce b := make([]byte, 16) _, err = rand.Read(b) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } bytes := hex.EncodeToString(b) // Set code-nonce pair in redis first redisClient.Set(code+"nonce", bytes, ttl) // Set code-phone_number pair redisClient.Set(code+"phone", phone, ttl) // Send SMS via Twilio data := url.Values{} data.Set("MessagingServiceSid", messagingSID) data.Set("To", phone) data.Set("Body", fmt.Sprintf("Your OTP for Beep is %s", code)) url := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", twilioSID) twilioReq, err := http.NewRequest("POST", url, strings.NewReader(data.Encode())) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } twilioReq.SetBasicAuth(twilioSID, twilioToken) twilioReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} resp, err := client.Do(twilioReq) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if resp.StatusCode < 200 || resp.StatusCode >= 300 { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Return nonce w.Write([]byte(bytes)) } func InitRequestBypass(w http.ResponseWriter, r *http.Request, p httprouter.Params) { // Get request body req := InitRequestBody{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&req) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // Make sure phone number is legitimate phone, err := ParsePhone(req.PhoneNumber) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // Generate nonce b := make([]byte, 16) _, err = rand.Read(b) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } bytes := hex.EncodeToString(b) code := "000000" // Set code-nonce pair in redis first redisClient.Set(code+"nonce", bytes, ttl) // Set code-phone_number pair redisClient.Set(code+"phone", phone, ttl) // Return nonce w.Write([]byte(bytes)) } type VerifyRequestBody struct { Code string `json:"code"` Nonce string `json:"nonce"` ClientId string `json:"clientid"` } func VerifyCode(w http.ResponseWriter, r *http.Request, p httprouter.Params) { // Get request body req := VerifyRequestBody{} decoder := json.NewDecoder(r.Body) err := decoder.Decode(&req) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // Get nonce storedNonce, err := redisClient.Get(req.Code + "nonce").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Delete nonce _, err = redisClient.Del(req.Code + "nonce").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Check nonce if req.Nonce != storedNonce { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } // Get stored phone number phoneNumber, err := redisClient.Get(req.Code + "phone").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Delete stored phone number _, err = redisClient.Del(req.Code + "phone").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Generate (potential) User ID userHex, err := RandomHex() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } userIDPotential := "u-" + userHex // Check for existing user var userID string err = db.QueryRow(` INSERT INTO "user" (id, first_name, last_name, phone_number) VALUES ($1, '', '', $2) ON CONFLICT(phone_number) DO UPDATE SET phone_number=EXCLUDED.phone_number RETURNING id `, userIDPotential, phoneNumber).Scan(&userID) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) log.Println(err) return } // Generate JWT token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ "userid": userID, "clientid": req.ClientId, }) tokenString, err := token.SignedString(privateKey) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } w.Write([]byte(tokenString)) } func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) { code := p.ByName("code") nonce := p.ByName("nonce") // Get nonce storedNonce, err := redisClient.Get(code + "nonce").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Delete nonce _, err = redisClient.Del(code + "nonce").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } // Check nonce if nonce != storedNonce { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } // Delete phone number _, err = redisClient.Del(code + "phone").Result() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } proxyReq, err := http.NewRequest(r.Method, coreURL, r.Body) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } proxyReq.Header.Set("X-User-Claim", dummyToken) for header, values := range r.Header { for _, value := range values { proxyReq.Header.Add(header, value) } } client := &http.Client{} proxyRes, err := client.Do(proxyReq) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } for header, values := range proxyRes.Header { for _, value := range values { w.Header().Add(header, value) } } io.Copy(w, proxyRes.Body) proxyRes.Body.Close() }