JWT Authentication with Golang GraphQL & Relay
October 11, 2016
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.