github.com/mstephano/gqlgen-schemagen@v0.0.0-20230113041936-dd2cd4ea46aa/docs/content/recipes/authentication.md (about) 1 --- 2 title: "Providing authentication details through context" 3 description: How to using golang context.Context to authenticate users and pass user data to resolvers. 4 linkTitle: Authentication 5 menu: { main: { parent: "recipes" } } 6 --- 7 8 We have an app where users are authenticated using a cookie in the HTTP request, and we want to check this authentication status somewhere in our graph. Because GraphQL is transport agnostic we can't assume there will even be an HTTP request, so we need to expose these authentication details to our graph using a middleware. 9 10 ```go 11 package auth 12 13 import ( 14 "database/sql" 15 "net/http" 16 "context" 17 ) 18 19 // A private key for context that only this package can access. This is important 20 // to prevent collisions between different context uses 21 var userCtxKey = &contextKey{"user"} 22 type contextKey struct { 23 name string 24 } 25 26 // A stand-in for our database backed user object 27 type User struct { 28 Name string 29 IsAdmin bool 30 } 31 32 // Middleware decodes the share session cookie and packs the session into context 33 func Middleware(db *sql.DB) func(http.Handler) http.Handler { 34 return func(next http.Handler) http.Handler { 35 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 c, err := r.Cookie("auth-cookie") 37 38 // Allow unauthenticated users in 39 if err != nil || c == nil { 40 next.ServeHTTP(w, r) 41 return 42 } 43 44 userId, err := validateAndGetUserID(c) 45 if err != nil { 46 http.Error(w, "Invalid cookie", http.StatusForbidden) 47 return 48 } 49 50 // get the user from the database 51 user := getUserByID(db, userId) 52 53 // put it in context 54 ctx := context.WithValue(r.Context(), userCtxKey, user) 55 56 // and call the next with our new context 57 r = r.WithContext(ctx) 58 next.ServeHTTP(w, r) 59 }) 60 } 61 } 62 63 // ForContext finds the user from the context. REQUIRES Middleware to have run. 64 func ForContext(ctx context.Context) *User { 65 raw, _ := ctx.Value(userCtxKey).(*User) 66 return raw 67 } 68 ``` 69 70 **Note:** `getUserByID` and `validateAndGetUserID` have been left to the user to implement. 71 72 Now when we create the server we should wrap it in our authentication middleware: 73 74 ```go 75 package main 76 77 import ( 78 "net/http" 79 80 "github.com/mstephano/gqlgen-schemagen/_examples/starwars" 81 "github.com/mstephano/gqlgen-schemagen/graphql/handler" 82 "github.com/mstephano/gqlgen-schemagen/graphql/playground" 83 "github.com/go-chi/chi" 84 ) 85 86 func main() { 87 router := chi.NewRouter() 88 89 router.Use(auth.Middleware(db)) 90 91 srv := handler.NewDefaultServer(starwars.NewExecutableSchema(starwars.NewResolver())) 92 router.Handle("/", playground.Handler("Starwars", "/query")) 93 router.Handle("/query", srv) 94 95 err := http.ListenAndServe(":8080", router) 96 if err != nil { 97 panic(err) 98 } 99 } 100 ``` 101 102 And in our resolvers (or directives) we can call `ForContext` to retrieve the data back out: 103 104 ```go 105 106 func (r *queryResolver) Hero(ctx context.Context, episode Episode) (Character, error) { 107 if user := auth.ForContext(ctx) ; user == nil || !user.IsAdmin { 108 return Character{}, fmt.Errorf("Access denied") 109 } 110 111 if episode == EpisodeEmpire { 112 return r.humans["1000"], nil 113 } 114 return r.droid["2001"], nil 115 } 116 ``` 117 118 ### Websockets 119 120 If you need access to the websocket init payload you can add your `InitFunc` in `AddTransport`. 121 Your InitFunc implementation could then attempt to extract items from the payload: 122 123 ```go 124 package main 125 126 import ( 127 "context" 128 "errors" 129 "log" 130 "net/http" 131 "os" 132 "time" 133 134 "github.com/mstephano/gqlgen-schemagen/graphql/handler" 135 "github.com/mstephano/gqlgen-schemagen/graphql/handler/extension" 136 "github.com/mstephano/gqlgen-schemagen/graphql/handler/transport" 137 "github.com/mstephano/gqlgen-schemagen/graphql/playground" 138 "github.com/go-chi/chi" 139 "github.com/gorilla/websocket" 140 "github.com/gqlgen/_examples/websocket-initfunc/server/graph" 141 "github.com/gqlgen/_examples/websocket-initfunc/server/graph/generated" 142 "github.com/rs/cors" 143 ) 144 145 func webSocketInit(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) { 146 // Get the token from payload 147 any := initPayload["authToken"] 148 token, ok := any.(string) 149 if !ok || token == "" { 150 return nil, errors.New("authToken not found in transport payload") 151 } 152 153 // Perform token verification and authentication... 154 userId := "john.doe" // e.g. userId, err := GetUserFromAuthentication(token) 155 156 // put it in context 157 ctxNew := context.WithValue(ctx, "username", userId) 158 159 return ctxNew, nil 160 } 161 162 const defaultPort = "8080" 163 164 func main() { 165 port := os.Getenv("PORT") 166 if port == "" { 167 port = defaultPort 168 } 169 170 router := chi.NewRouter() 171 172 // CORS setup, allow any for now 173 // https://gqlgen.com/recipes/cors/ 174 c := cors.New(cors.Options{ 175 AllowedOrigins: []string{"*"}, 176 AllowCredentials: true, 177 Debug: false, 178 }) 179 180 srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 181 srv.AddTransport(transport.POST{}) 182 srv.AddTransport(transport.Websocket{ 183 KeepAlivePingInterval: 10 * time.Second, 184 Upgrader: websocket.Upgrader{ 185 CheckOrigin: func(r *http.Request) bool { 186 return true 187 }, 188 }, 189 InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, error) { 190 return webSocketInit(ctx, initPayload) 191 }, 192 }) 193 srv.Use(extension.Introspection{}) 194 195 router.Handle("/", playground.Handler("My GraphQL App", "/app")) 196 router.Handle("/app", c.Handler(srv)) 197 198 log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 199 log.Fatal(http.ListenAndServe(":"+port, router)) 200 } 201 ``` 202 203 > Note 204 > 205 > Subscriptions are long lived, if your tokens can timeout or need to be refreshed you should keep the token in 206 > context too and verify it is still valid in `auth.ForContext`.