├── .gitignore ├── LICENSE ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | gauth 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Gustavo Rodríguez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimalistic Go JWT Auth example 2 | 3 | #### Dependencies 4 | + `github.com/dgrijalva/jwt-go` 5 | + `github.com/garyburd/redigo/redis` 6 | + `github.com/gorilla/mux` 7 | + `golang.org/x/crypto/bcrypt` 8 | 9 | #### Installation 10 | + Install [redis](https://redis.io) 11 | + Clone repo to `$GOPATH/src/github.com/octohedron/gauth` 12 | + Install dependencies 13 | ```Bash 14 | $ go get 15 | ``` 16 | + Set environment variables 17 | ```Bash 18 | $ export AUTH_PORT=YOUR_PORT # i.e. 8000 19 | $ export SIGN_KEY=secret # your jwt sign key 20 | ``` 21 | #### Usage 22 | ```Bash 23 | $ go build && ./gauth 24 | ``` 25 | This will run the server and you can try it out with curl 26 | 27 | #### Register 28 | ```Bash 29 | $ curl -X POST -F 'email=a@a.com' -F 'password=password' http://192.168.1.43:4200/register 30 | # Should print out a token, similar to 31 | { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ... " } 32 | ``` 33 | 34 | 35 | #### Login 36 | ```Bash 37 | $ curl -X POST -F 'email=a@a.com' -F 'password=password' http://192.168.1.43:4200/login 38 | # Should print out a token, similar to 39 | { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ... " } 40 | ``` 41 | 42 | Example AJAX usage 43 | 44 | ```JavaScript 45 | // 46 | // Note: For this to work you might need to uncomment the CORS headers in the 47 | // setHeaders func 48 | // 49 | // button, on click ... 50 | var formData = new FormData(); 51 | formData.append("email", "a@a.com"); 52 | formData.append("password", "hunter2"); 53 | // make the request 54 | fetch("http://192.168.1.43:4200/login", { 55 | method: "POST", 56 | body: formData 57 | }).then(result => { 58 | result.json().then(result => { 59 | if (result.error) { 60 | alert(result.error); // "Wrong password" or "Email not found" 61 | } else { 62 | alert("Your token is: " + result.access_token); 63 | } 64 | }); 65 | }); 66 | ``` 67 | 68 | LICENSE: MIT -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/dgrijalva/jwt-go" 12 | "github.com/garyburd/redigo/redis" 13 | "github.com/gorilla/mux" 14 | "golang.org/x/crypto/bcrypt" 15 | ) 16 | 17 | // server type is for sharing dependencies with handlers 18 | type server struct { 19 | pool *redis.Pool 20 | router *mux.Router 21 | } 22 | 23 | // POOL - Declare a global variable to store the Redis connection pool 24 | var POOL *redis.Pool 25 | 26 | // PORT - The running port for this service 27 | var PORT = "" 28 | 29 | // signKey - The JWT Sign key 30 | var signKey = []byte("secret") 31 | 32 | // Loads the default variables 33 | func init() { 34 | POOL = newPool("localhost:6379") 35 | PORT = os.Getenv("AUTH_PORT") 36 | if PORT == "" { 37 | PORT = "4000" 38 | } 39 | signKey = []byte(os.Getenv("signKey")) 40 | } 41 | 42 | // Returns a pointer to a redis pool 43 | func newPool(addr string) *redis.Pool { 44 | return &redis.Pool{ 45 | MaxIdle: 5, 46 | IdleTimeout: 240 * time.Second, 47 | Dial: func() (redis.Conn, error) { return redis.Dial("tcp", addr) }, 48 | } 49 | } 50 | 51 | func (s *server) routes() { 52 | s.router.HandleFunc("/login", s.handleLogin()) 53 | s.router.HandleFunc("/register", s.handleRegister()) 54 | } 55 | 56 | // Gets you a token if you pass the right credentials 57 | func (s *server) handleLogin() http.HandlerFunc { 58 | return func(w http.ResponseWriter, r *http.Request) { 59 | var err error 60 | setHeaders(w) 61 | if r.Method != "POST" { 62 | http.Error(w, fmt.Sprintf("{ \"error\": \"%s\" }", "Forbidden request"), 403) 63 | return 64 | } 65 | conn := s.pool.Get() 66 | defer conn.Close() 67 | email := strings.ToLower(r.FormValue("email")) 68 | password, err := redis.Bytes(conn.Do("GET", email)) 69 | if err == nil { 70 | // compare passwords 71 | err = bcrypt.CompareHashAndPassword(password, []byte(r.FormValue("password"))) 72 | // if it doesn't match 73 | if err != nil { 74 | http.Error(w, fmt.Sprintf("{ \"error\": \"%s\" }", "Wrong password"), 401) 75 | return 76 | } 77 | token := jwt.New(jwt.SigningMethodHS256) 78 | claims := token.Claims.(jwt.MapClaims) 79 | claims["admin"] = false 80 | claims["email"] = email 81 | // 24 hour token 82 | claims["exp"] = time.Now().Add(time.Hour * 24).Unix() 83 | tokenString, _ := token.SignedString(signKey) 84 | w.Write([]byte(fmt.Sprintf("{ \"access_token\": \"%s\" }", tokenString))) 85 | } else { 86 | // email not found 87 | http.Error(w, fmt.Sprintf("{ \"error\": \"%s\" }", "Email not found"), 401) 88 | } 89 | } 90 | } 91 | 92 | // Register a new user, gives you a token and sets the email -> password 93 | // in redis if the email doesn't exist 94 | func (s *server) handleRegister() http.HandlerFunc { 95 | return func(w http.ResponseWriter, r *http.Request) { 96 | var err error 97 | setHeaders(w) 98 | if r.Method != "POST" { 99 | http.Error(w, fmt.Sprintf("{ \"error\": \"%s\" }", "Forbidden request"), 403) 100 | return 101 | } 102 | conn := s.pool.Get() 103 | defer conn.Close() 104 | email := strings.ToLower(r.FormValue("email")) 105 | // check if the user is already registered 106 | exists, err := redis.Bool(conn.Do("EXISTS", email)) 107 | if exists { 108 | w.Write([]byte(fmt.Sprintf("{ \"error\": \"%s\" }", "Email taken"))) 109 | return 110 | } 111 | // get password from the post request form value 112 | password := r.FormValue("password") 113 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 114 | bcrypt.DefaultCost) 115 | if err != nil { 116 | panic(err) 117 | } 118 | // Set email -> password in redis 119 | _, err = conn.Do("SET", email, string(hashedPassword[:])) 120 | if err != nil { 121 | log.Println(err) 122 | } 123 | token := jwt.New(jwt.SigningMethodHS256) 124 | claims := token.Claims.(jwt.MapClaims) 125 | claims["admin"] = false 126 | claims["email"] = email 127 | // 24 hour token 128 | claims["exp"] = time.Now().Add(time.Hour * 24).Unix() 129 | tokenString, _ := token.SignedString(signKey) 130 | w.Write([]byte(fmt.Sprintf("{ \"access_token\": \"%s\" }", tokenString))) 131 | } 132 | } 133 | 134 | func setHeaders(w http.ResponseWriter) http.ResponseWriter { 135 | w.Header().Set("Content-Type", "application/json") 136 | // Uncomment the following lines if you are having CORS issues, 137 | // optionally, replace "*" with your preferred address 138 | // 139 | // w.Header().Set("Access-Control-Allow-Origin", "*") 140 | // w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") 141 | // w.Header().Set("Access-Control-Allow-Headers", 142 | // "Accept, 0, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 143 | return w 144 | } 145 | 146 | func main() { 147 | sr := &server{ 148 | router: mux.NewRouter(), 149 | pool: POOL, 150 | } 151 | sr.routes() 152 | srv := &http.Server{ 153 | Handler: sr.router, 154 | Addr: ":" + PORT, 155 | WriteTimeout: 5 * time.Second, 156 | ReadTimeout: 5 * time.Second, 157 | } 158 | log.Println("Auth server running at", PORT) 159 | err := srv.ListenAndServe() 160 | if err != nil { 161 | log.Fatal("ListenAndServe: ", err) 162 | } 163 | } 164 | --------------------------------------------------------------------------------