5
0
Fork 0

feat: Improved bypass

feat/improved-bypass
Ambrose Chua 2019-07-27 18:13:25 +08:00
parent 217c90d0cf
commit a33b79712f
Signed by: ambrose
GPG Key ID: B34FBE029276BA5D
2 changed files with 247 additions and 241 deletions

View File

@ -9,6 +9,7 @@ RUN CGO_ENABLED=0 go build -ldflags "-s -w"
FROM scratch FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /src/login /login COPY --from=build /src/login /login
COPY --from=build /src/.env /.env COPY --from=build /src/.env /.env

487
main.go
View File

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