In a Golang app, I'm using gorilla/sessions with the mySQL backend to store data in a session but I'd like to store the data in the chi router context instead. How do I add auth token strings or structs into the context? I see plenty of examples explaining how to read/use them but none I saw explain how to insert the data into the context within a login function.
For example, the chi documentation here has the following code to check admin user which will work for me but it offers no clue as to how the auth object got into the context in the first place. There is another middleware function describing how to load an article struct into the context and gets its id parameter from the URL but that wont work for me. I don't really want to use a cookie as that would defeat the whole purpose in using the context.
// AdminOnly middleware restricts access to just administrators.
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isAdmin, ok := r.Context().Value("acl.admin").(bool)
if !ok || !isAdmin {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
In the example below I want to store the User struct or an auth token in the context.
Additionally, how would I handle flashes in the context? Can I do that or should I just stick to the db based sessions for web apps?
Ideally, I want to use the same method for both a web app serving templates and a separate API app.
Here is my web app login code, simplified a bit to remove the cruft:
func (rs *appResource) login(w http.ResponseWriter, r *http.Request) {
// appResource contains db connection, session store and templates
var authUser AuthUser //struct
session, err := rs.store.Get(r, "admin-data")
email := r.FormValue("email")
password := r.FormValue("password")
sqlStatement := "SELECT id, venue_id, first_name, last_name, email, password FROM Users WHERE email=?"
row := rs.db.QueryRow(sqlStatement, email)
err = row.Scan(&authUser.UserID, &authUser.VenueID, &authUser.FirstName, &authUser.LastName, &authUser.Email, &authUser.Password)
if err = bcrypt.CompareHashAndPassword([]byte(authUser.Password), []byte(password)); err != nil {
session.AddFlash("Your password is incorrect")
session.Save(r, w)
http.Redirect(w, r, "/signin", http.StatusFound)
return
}
authUser.Authenticated = true
session.Values["authUser"] = authUser
firstName := authUser.FirstName
message := fmt.Sprintf("Welcome, %s!", firstName)
session.AddFlash(message)
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusFound)
}
From the go documentation about context, to set a value in the context, you simply need to use WithValue.
To give you an idea, the simplest example would be:
package main
import (
"fmt"
"context"
)
func main() {
const key = "myKey"
ctx := context.Background()
ctx := context.WithValue(ctx, key, "someVal")
v, ok := ctx.Value(key).(string)
if !ok {
fmt.Println("key does not exist in the context")
return
}
fmt.Printf("found %v", v)
}
Now, following the example, setting a new value in the context is pretty easy. When we are talking about r.Context()
, the context coming out from it has been previously set in the *http.Request
request using WithContext. Just a quick example:
package main
import (
"http"
"context"
)
func main() {
req, err := http.NewRequest(
http.MethodGet,
"/someEdp",
nil,
)
if err != nil {
fmt.Println(err)
return
}
ctx := context.Background()
ctx := context.WithValue(ctx, "auth-token", "mySecretToken")
req = req.WithContext(ctx)
// Do the request
}
And to read it in your handler it would be as easy as:
func (rs *appResource) login(w http.ResponseWriter, r *http.Request) {
...
ctx := r.Context()
v, ok := ctx.Value("auth-token").(string)
if !ok {
fmt.Println("ups")
return
}
fmt.Printf("found %v", v)
// Do something
}
Now, if you want to use this mechanism to safely store & read an authorization token in the context, I suggest you to take a look at this article about context keys written by @Mat Ryer.
Basically, anyone who "intercepts" your request, can potentially read the authorization token from your request because you are using a string as a key.
Instead, you should define your private context key that will allow you to set/read the authorization token from the context.
package main
import (
"fmt"
"context"
)
type contextKey string
var contextKeyAuthtoken = contextKey("auth-token")
func setToken(ctx context.Context, token string) context.Context {
return context.WithValue(ctx, contextKeyAuthtoken, token)
}
func getToken(ctx context.Context) (string, bool) {
tokenStr, ok := ctx.Value(contextKeyAuthtoken).(string)
return tokenStr, ok
}
func main() {
ctx := context.Background()
ctx = setToken(ctx, "someToken")
t, ok := getToken(ctx)
if !ok {
fmt.Println("unauthorized")
return
}
fmt.Printf("authorized with token %v", t)
}
Here's the playground I set for the snippet above.
So, you would use getToken
in the consumer of the request (your receiver handler) & setToken
in another service (?) or another logical component of your service.
In this way you, and only you, will be able to read from the context your authorization token and allow the user to do something or not.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With