From b9b825ec6bc3a6fb09ac7e6cf298f6b3233c11bf Mon Sep 17 00:00:00 2001 From: Ishan Jain Date: Thu, 5 Oct 2017 11:03:57 +0530 Subject: [PATCH] Completed Cookie based sessions, Refactored and fixed bugs --- auth/auth.go | 186 +++++++++++++++++++++++++++++++++++++++-------- router/router.go | 19 +++-- 2 files changed, 167 insertions(+), 38 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index f354faa..04f4527 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -3,9 +3,13 @@ package auth import ( "crypto/aes" "crypto/cipher" + "crypto/hmac" "crypto/rand" + "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" "log" "net/http" @@ -14,69 +18,168 @@ import ( "github.com/ishanjain28/pogo/common" ) +const ( + enc = "cookie_session_encryption" + // mac = "cookie_session_signature" + + // This is the key with which each cookie is encrypted, I'll recommend moving it to a env file. + secret = "super_long_string_difficult_to_crack" + cookieName = "POGO_SESSION" + cookieExpiry = 60 * 60 * 24 * 30 // 30 days in seconds +) + func RequireAuthorization() common.Handler { return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { - if usr := decryptSession(r); usr != nil { - rc.User = usr - return nil - } - - if strings.Contains(r.Header.Get("Accept"), "html") || r.Method == "GET" { - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) - return nil - } else { + usr, err := decryptCookie(r) + if err != nil { + fmt.Println(err.Error()) + if strings.Contains(r.Header.Get("Accept"), "html") || r.Method == "GET" { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + return &common.HTTPError{ + Message: "Unauthorized! Redirecting to /login", + StatusCode: http.StatusTemporaryRedirect, + } + } return &common.HTTPError{ Message: "Unauthorized!", StatusCode: http.StatusUnauthorized, } } - - return &common.HTTPError{ - Message: "Unauthorized!", - StatusCode: http.StatusUnauthorized, - } + rc.User = usr + return nil } } -func CreateSession(u *common.User, w http.ResponseWriter) error { - - // n_J6vaKjmmw4WB95DMorjQ.UMYdBLfttwPgQw9T0u0wdK7bGwDT9vwxoPAKWhjSAcpoiMsjh4eSfBkA4WB2deSoQu_cjCaJrcp77rvG67xkOeXsYpiclx2b-Oi7MHM3Kms.1507140277977.604800000.2CdxwiKAJT4SYJTVK-Du5jokr-CCnxo1ukdaVBkLRJg +func CreateSession(u *common.User) (*http.Cookie, error) { iv, err := generateRandomString(16) if err != nil { - return err + return nil, err } userJSON, err := json.Marshal(u) if err != nil { - return err - + return nil, err } - var hexedJSON []byte - hex.Encode(hexedJSON, userJSON) - fmt.Println(iv, string(userJSON), hexedJSON) + hexedJSON := hex.EncodeToString(userJSON) - block, err := aes.NewCipher(hexedJSON) + encKey := deriveKey(enc, secret) + + block, err := aes.NewCipher(encKey) if err != nil { - return err + return nil, err } - mode := cipher.NewCBCEncrypter(block, iv) - return nil + // Fill the block with 0x0e + if remBytes := len(hexedJSON) % aes.BlockSize; remBytes != 0 { + t := []byte(hexedJSON) + + for i := 0; i < aes.BlockSize-remBytes; i++ { + t = append(t, 0x0e) + } + hexedJSON = string(t) + } + + mode := cipher.NewCBCEncrypter(block, iv) + encCipher := make([]byte, len(hexedJSON)+aes.BlockSize) + + mode.CryptBlocks(encCipher, []byte(hexedJSON)) + + cipherbase64 := base64urlencode(encCipher) + ivbase64 := base64urlencode(iv) + + // Cookie format: iv.cipher.created_on.expire_on.HMAC + cookieStr := fmt.Sprintf("%s.%s", ivbase64, cipherbase64) + + c := &http.Cookie{ + Name: cookieName, + Value: cookieStr, + MaxAge: cookieExpiry, + } + + return c, nil } -func decryptSession(r *http.Request) *common.User { +func decryptCookie(r *http.Request) (*common.User, error) { - c, err := r.Cookie("POGO_SESSION") + c, err := r.Cookie(cookieName) if err != nil { if err != http.ErrNoCookie { log.Printf("error in reading Cookie: %v", err) } - return nil + return nil, err } - fmt.Println(c) - return nil + csplit := strings.Split(c.Value, ".") + if len(csplit) != 2 { + return nil, errors.New("Invalid number of values in cookie") + } + + ivb, cipherb := csplit[0], csplit[1] + + iv, err := base64urldecode(ivb) + if err != nil { + return nil, err + } + dcipher, err := base64urldecode(cipherb) + if err != nil { + return nil, err + } + + if len(iv) != 16 { + return nil, errors.New("IV length is not 16") + } + + encKey := deriveKey(enc, secret) + + if len(dcipher)%aes.BlockSize != 0 { + return nil, errors.New("ciphertext not multiple of blocksize") + } + + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + buf := make([]byte, len(dcipher)) + + mode := cipher.NewCBCDecrypter(block, iv) + + mode.CryptBlocks(buf, []byte(dcipher)) + + tstr := fmt.Sprintf("%x", buf) + + // Remove aes padding, 0e is used because it was used in encryption to mark padding + padIndex := strings.Index(tstr, "0e") + if padIndex == -1 { + return nil, errors.New("Padding Index is -1") + } + tstr = tstr[:padIndex] + + data, err := hex.DecodeString(tstr) + if err != nil { + return nil, err + } + + data, err = hex.DecodeString(string(data)) + if err != nil { + return nil, err + } + + u := &common.User{} + err = json.Unmarshal(data, u) + if err != nil { + return nil, err + } + + return u, nil +} + +func deriveKey(msg, secret string) []byte { + key := []byte(secret) + sha256hash := hmac.New(sha256.New, key) + sha256hash.Write([]byte(msg)) + + return sha256hash.Sum(nil) } func generateRandomString(l int) ([]byte, error) { @@ -88,3 +191,22 @@ func generateRandomString(l int) ([]byte, error) { } return rBytes, nil } + +func base64urldecode(str string) ([]byte, error) { + base64str := strings.Replace(string(str), "-", "+", -1) + base64str = strings.Replace(base64str, "_", "/", -1) + + s, err := base64.RawStdEncoding.DecodeString(base64str) + if err != nil { + return nil, err + } + + return s, nil +} + +func base64urlencode(str []byte) string { + base64str := strings.Replace(string(str), "+", "-", -1) + base64str = strings.Replace(base64str, "/", "_", -1) + + return base64.RawStdEncoding.EncodeToString([]byte(base64str)) +} diff --git a/router/router.go b/router/router.go index 5a63fc3..5039101 100644 --- a/router/router.go +++ b/router/router.go @@ -65,13 +65,13 @@ func Init() *mux.Router { // Authenticated endpoints should be passed to BasicAuth() // first r.Handle("/admin", Handle( - // auth.RequireAuthorization(), + auth.RequireAuthorization(), adminHandler(), - )) + )).Methods("GET", "POST") r.Handle("/login", Handle( loginHandler(), - )) + )).Methods("GET", "POST") // r.HandleFunc("/admin/publish", BasicAuth(CreateEpisode)) // r.HandleFunc("/admin/delete", BasicAuth(RemoveEpisode)) @@ -87,6 +87,11 @@ func Init() *mux.Router { func loginHandler() common.Handler { return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + if _, err := r.Cookie("POGO_SESSION"); err == nil { + http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect) + return nil + } + if r.Method == "GET" { w.Header().Set("Content-Type", "text/html") return common.ReadAndServeFile("assets/web/login.html", w) @@ -99,7 +104,6 @@ func loginHandler() common.Handler { Message: fmt.Sprintf("error in reading users.json: %v", err), StatusCode: http.StatusInternalServerError, } - } err = r.ParseForm() @@ -127,15 +131,18 @@ func loginHandler() common.Handler { for k, v := range u { if k == username && v == password { // Create a cookie here because the credentials are correct - err = auth.CreateSession(&common.User{ + c, err := auth.CreateSession(&common.User{ Username: k, - }, w) + }) if err != nil { return &common.HTTPError{ Message: err.Error(), StatusCode: http.StatusInternalServerError, } } + + // r.AddCookie(c) + w.Header().Add("Set-Cookie", c.String()) // And now redirect the user to admin page http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect) return nil