Boone Putney bio photo

Boone Putney

Software Development
Random Musings
Austin, Texas

HumanPlanet Soleer

Email LinkedIn Github

Background

github.com/graphql-go/graphql and github.com/graphql-go/relay provide a great starting point for creating your own Relay-compliant GraphQL server with Go. Authorization, however, is left up completely to the developer. Here is how we implemented a /login endpoint and passed the resulting context to the GraphQL Handler.

We’ll leave the handling of these context values within GraphQL to another post…

Code

  1 package main
  2 
  3 import (
  4   "encoding/json"
  5   "net/http"
  6 
  7   jwt "github.com/dgrijalva/jwt-go"
  8   "github.com/graphql-go/graphql"
  9   "github.com/graphql-go/handler"
 10   "github.com/jinzhu/gorm"
 11   _ "github.com/jinzhu/gorm/dialects/postgres"
 12 )
 13 
 14 func main() {
 15   setupServer()
 16 }
 17 
 18 func setupMux() *http.ServeMux {
 19   mux := http.NewServeMux()
 20 
 21   // graphql Handler
 22   graphqlHandler := http.HandlerFunc(graphqlHandlerFunc)
 23 
 24   // login Handler
 25   mux.HandleFunc("/login", loginFunc)
 26 
 27   // add in addContext middlware
 28   mux.Handle("/graphql", requireAuth(graphqlHandler))
 29 
 30   return mux
 31 }
 32 
 33 func setupServer() {
 34   http.ListenAndServe(":8080", setupMux())
 35 }
 36 
 37 // graphqlHandlerFunc creates the graphql handler
 38 func graphqlHandlerFunc(w http.ResponseWriter, r *http.Request) {
 39   // get query
 40   opts := handler.NewRequestOptions(r)
 41 
 42   // execute graphql query
 43   params := graphql.Params{
 44     Schema:         Schema, // defined in another file
 45     RequestString:  opts.Query,
 46     VariableValues: opts.Variables,
 47     OperationName:  opts.OperationName,
 48     Context:        r.Context(), // pass http.Request.Context() to our graphql object
 49   }
 50   result := graphql.Do(params)
 51 
 52   // output JSON
 53   var buff []byte
 54   w.WriteHeader(http.StatusOK)
 55   if prettyPrintGraphQL {
 56     buff, _ = json.MarshalIndent(result, "", "\t")
 57   } else {
 58     buff, _ = json.Marshal(result)
 59   }
 60   w.Write(buff)
 61 }
 62 
 63 // type definition for our claims
 64 type Claims struct {
 65 	UserID  uint64 `json:"userID"`
 66 	IsAdmin bool   `json:"isAdmin"`
 67 	jwt.StandardClaims
 68 }
 69 
 70 // secret string for signing requests
 71 var jwtSecret = []byte("secret") // make sure you change this to something secure
 72 
 73 // key type is not exported to prevent collisions with context keys defined in
 74 // other packages.
 75 type key int
 76 
 77 // userAuthKey is the context key for our added struct.  Its value of zero is
 78 // arbitrary.  If this package defined other context keys, they would have
 79 // different integer values.
 80 const userAuthKey key = 0
 81 
 82 // validate JWT submitted via Authorization Header and set context claims
 83 func requireAuth(next http.Handler) http.Handler {
 84 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 85 		// extract jwt
 86 		authorizationHeader := r.Header.Get("Authorization")
 87 		authRegex, _ := regexp.Compile("(?:Bearer *)([^ ]+)(?: *)")
 88 		authRegexMatches := authRegex.FindStringSubmatch(authorizationHeader)
 89 		if len(authRegexMatches) != 2 {
 90 			// didn't match valid Authorization header pattern
 91 			httpError(w, "not authorized", http.StatusUnauthorized)
 92 			return
 93 		}
 94 		jwtToken := authRegexMatches[1]
 95 
 96 		// parse tokentoken
 97 		token, err := jwt.ParseWithClaims(jwtToken, &Claims{}, func(token *jwt.Token) (interface{}, error) {
 98 			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 99 				return nil, fmt.Errorf("Unexpected signing method")
100 			}
101 			return jwtSecret, nil
102 		})
103 		if err != nil {
104 			httpError(w, "not authorized", http.StatusUnauthorized)
105 			return
106 		}
107 
108 		// extract claims
109 		claims, ok := token.Claims.(*Claims)
110 		if !ok || !token.Valid {
111 			httpError(w, "not authorized", http.StatusUnauthorized)
112 			return
113 		}
114 
115 		// load userID & isAdmin into context
116 		authContext := struct {
117 			UserID  uint64 `json:"userId"`
118 			IsAdmin bool   `json:"isAdmin"`
119 		}{
120 			claims.UserID,
121 			claims.IsAdmin,
122 		}
123 		ctx := context.WithValue(r.Context(), userAuthKey, authContext)
124 		next.ServeHTTP(w, r.WithContext(ctx))
125 	})
126 }
127 
128 // loginFunc confirms login credentials and creates JWT if valid
129 func loginFunc(w http.ResponseWriter, req *http.Request) {
130 	// get username & password
131 	decoder := json.NewDecoder(req.Body)
132 	requestBody := struct {
133 		Username string `json:"username"`
134 		Password string `json:"password"`
135 	}{}
136 	err := decoder.Decode(&requestBody)
137 	if err != nil {
138 		http.Error(w, err.Error(), http.StatusInternalServerError)
139 		return
140 	}
141 	defer req.Body.Close()
142 
143 	// confirmLogin is up to you to define
144     user, err := confirmLogin(requestBody)
145     if err != nil {
146         http.Error(w, "invalid login", http.StatusUnauthorized)
147 		return
148     }
149 
150 	//generate token
151 	expireToken := time.Now().Add(time.Hour * 1).Unix()
152 	claims := Claims{
153 		user.ID,
154 		user.IsAdmin,
155 		jwt.StandardClaims{
156 			ExpiresAt: expireToken,
157 			Issuer:    "localhost:8080",
158 		},
159 	}
160 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
161 	signedToken, _ := token.SignedString(jwtSecret)
162 
163 	//output token
164 	tokenResponse := struct {
165 		Token string `json:"token"`
166 	}{signedToken}
167 	json.NewEncoder(w).Encode(tokenResponse)
168 }

Explanation

The loginFunc() function at /login receives the posted request and extracts a username and password from it. ConfirmLogin() is not outlined above, but is easy enough to implement. In our scenario, we validate the login against our database and return the user or an error. If the login is valid, we create and return the JWT with our encoded custom claim values (UserID & IsAdmin).

When a request is made to the graphql handler, the requireAuth() middleware parses the JWT, and if valid, passes the (UserID & IsAdmin) variables via context.

The passing of context to GraphQL is explained in this post.