auth.go (3811B)
1 package http 2 3 import ( 4 "crypto/rand" 5 "crypto/sha256" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "net/http" 11 ) 12 13 // Deal with login, logout, and general security stuff 14 15 const ( 16 keyAuthenticated = "authenticated" 17 keyCSRFToken = "csrfToken" 18 keyPassphrase = "passphrase" 19 keySessionName = "session" 20 headerCSRFToken = "X-Csrf-Token" 21 headerPassHash = "Pass-Hash" 22 ) 23 24 // initHandler checks two things: 25 // 1. If a CSRF token exists for the given session. Otherwise it creates it 26 // 2. Whether the session is authenticated 27 // It then returns an object on the form {"auth": true, "csrftoken": "<long string>", "pagetitle": "Page title"} 28 // This is requested immediately when the website is loaded. 29 func initHandler(w http.ResponseWriter, r *http.Request) { 30 session, _ := store.Get(r, keySessionName) 31 val := session.Values[keyCSRFToken] 32 token, ok := val.(string) 33 if !ok { 34 token = generateCSRFToken() 35 session.Values[keyCSRFToken] = token 36 session.Save(r, w) // TODO: Error handling, as always 37 } 38 b, _ := json.Marshal(struct { 39 PageTitle string `json:"pagetitle"` 40 CSRFToken string `json:"csrftoken"` 41 Authenticated bool `json:"authenticated"` 42 }{ 43 string(conf.PageTitle), 44 token, 45 authorized(r), 46 }) 47 w.Write(b) 48 } 49 50 func loginHandler(w http.ResponseWriter, r *http.Request) { 51 if authorized(r) { 52 return 53 } 54 defer r.Body.Close() // needed? 55 content, err := ioutil.ReadAll(r.Body) 56 check(err) 57 var data map[string]string 58 json.Unmarshal(content, &data) 59 passphrase := data[keyPassphrase] 60 passhash := sha256.Sum256([]byte(passphrase)) 61 // Converting byte slices to string is fine, it's how bytes.Equal does it 62 if string(passhash[:]) == string(conf.PassHash) { 63 session, _ := store.Get(r, keySessionName) 64 session.Values[keyAuthenticated] = true 65 err = session.Save(r, w) 66 check(err) 67 w.WriteHeader(http.StatusOK) // Not needed, just for readability? 68 } else { 69 w.WriteHeader(http.StatusUnauthorized) 70 } 71 } 72 73 func logoutHandler(w http.ResponseWriter, r *http.Request) { 74 session, _ := store.Get(r, keySessionName) 75 session.Values[keyAuthenticated] = false 76 err := session.Save(r, w) 77 check(err) 78 b, _ := json.Marshal(struct { 79 Info string `json:"info"` 80 }{"Logged out"}) 81 w.Write(b) 82 } 83 84 func authorized(r *http.Request) bool { 85 session, _ := store.Get(r, keySessionName) 86 val := session.Values[keyAuthenticated] 87 auth, ok := val.(bool) 88 return ok && auth 89 } 90 91 // Middleware for the simple sermoni authentication scheme 92 // Accepts authentication both with session cookie (typically web browser) and 93 // a header on the format `Pass-Hash: <sha256sum of pass phrase>` 94 func auth(handler http.HandlerFunc) http.Handler { 95 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 96 passHashHeader := r.Header[headerPassHash] 97 if passHashHeader != nil { 98 // compare to hex formatted version of correct hash 99 if passHashHeader[0] != fmt.Sprintf("%x", conf.PassHash) { 100 status := http.StatusUnauthorized 101 http.Error(w, "Invalid passphrase hash", status) 102 return 103 } 104 } else { 105 // store is the global CookieStore 106 if !authorized(r) || !csrfCheckPassed(r) { 107 status := http.StatusUnauthorized 108 http.Error(w, http.StatusText(status), status) 109 return 110 } 111 } 112 handler.ServeHTTP(w, r) 113 }) 114 } 115 116 func csrfCheckPassed(r *http.Request) bool { 117 session, _ := store.Get(r, keySessionName) 118 // CSRF protect anything but GET requests 119 if r.Method == http.MethodGet { 120 return true 121 } 122 val := session.Values[keyCSRFToken] 123 rightToken, ok := val.(string) 124 if !ok { 125 panic("no CSRF token found") 126 } 127 tokenHeader := r.Header[headerCSRFToken] 128 if tokenHeader == nil { 129 return false 130 } 131 return tokenHeader[0] == rightToken 132 } 133 134 func generateCSRFToken() string { 135 randBytes := make([]byte, 32) 136 rand.Read(randBytes) 137 return hex.EncodeToString(randBytes) 138 }