From a3e66cd3515c4404d5bab2438337f1d541d5efa0 Mon Sep 17 00:00:00 2001 From: Djeeberjr Date: Fri, 24 Sep 2021 01:27:19 +0200 Subject: [PATCH] jwt refresh --- internal/helper.go | 16 ++++++ internal/httpServer.go | 123 +++++++++++++++++++++++++---------------- internal/mutations.go | 9 +-- 3 files changed, 91 insertions(+), 57 deletions(-) diff --git a/internal/helper.go b/internal/helper.go index 24f717b..06c9ace 100644 --- a/internal/helper.go +++ b/internal/helper.go @@ -5,6 +5,7 @@ import ( "fmt" "path/filepath" "strings" + "time" "github.com/golang-jwt/jwt" "github.com/graph-gophers/dataloader" @@ -123,3 +124,18 @@ func isAuth(ctx context.Context) (bool, error) { return false, extendError("UNAUTHORIZED", "Unauthorized") } } + +func createJWT(claims *JWTClaims) *jwt.Token { + + claims.ExpiresAt = time.Now().Add(time.Hour * 24).Unix() + + return jwt.NewWithClaims(jwt.SigningMethodHS256, claims) +} + +func createClaims(username string) *JWTClaims { + return &JWTClaims{ + StandardClaims: jwt.StandardClaims{ + Subject: username, + }, + } +} diff --git a/internal/httpServer.go b/internal/httpServer.go index 6f94e71..3319b21 100644 --- a/internal/httpServer.go +++ b/internal/httpServer.go @@ -7,10 +7,10 @@ import ( "mime" "net/http" "path/filepath" - "strings" "time" "github.com/golang-jwt/jwt" + jwtRequest "github.com/golang-jwt/jwt/request" "github.com/gorilla/mux" "github.com/graphql-go/graphql" "github.com/graphql-go/graphql/gqlerrors" @@ -24,6 +24,21 @@ type JWTClaims struct { jwt.StandardClaims } +type CookieExtractor struct { + Name string +} + +func (c *CookieExtractor) ExtractToken(req *http.Request) (string, error) { + cookie, err := req.Cookie(c.Name) + + if err == nil && len(cookie.Value) != 0 { + return cookie.Value, nil + } + + return "", jwtRequest.ErrNoTokenInRequest + +} + // initHttp setup and start the http server. Blocking func initHttp(resolveContext context.Context, schema graphql.Schema, address string) error { r := mux.NewRouter() @@ -47,16 +62,15 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str r.Use(func(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - token := getTokenFromRequest(r) + parsedToken, err := jwtRequest.ParseFromRequestWithClaims(r, jwtRequest.MultiExtractor{ + jwtRequest.AuthorizationHeaderExtractor, + &CookieExtractor{Name: "jwt"}, + }, &JWTClaims{}, jwtKeyFunc) - if token != "" { - parsedToken, err := parseJWT(token) - - if err == nil && parsedToken.Valid { - newRequest := r.WithContext(context.WithValue(r.Context(), "jwt", parsedToken)) - h.ServeHTTP(rw, newRequest) - return - } + if err == nil && parsedToken.Valid { + newRequest := r.WithContext(context.WithValue(r.Context(), "jwt", parsedToken)) + h.ServeHTTP(rw, newRequest) + return } h.ServeHTTP(rw, r) @@ -80,6 +94,8 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str r.HandleFunc("/api/logout", logout).Methods("POST") + r.HandleFunc("/api/refresh", refreshToken).Methods("POST") + // Init the embedded static files initStatic(r) @@ -161,43 +177,11 @@ func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) rw.WriteHeader(http.StatusCreated) } -//parseJWT parse a JWT. does not check if token is valid. Returns error if non was provided -func parseJWT(token string) (*jwt.Token, error) { - if token == "" { - return nil, fmt.Errorf("No token provided") +func jwtKeyFunc(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"]) } - - parsed, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - return []byte("TODO"), nil - }) - - if err != nil { - return nil, err - } - - return parsed, nil - -} - -//getTokenFromRequest looks for a JWT in the "Authorization" header or in the cookies -func getTokenFromRequest(r *http.Request) string { - // get token from header - authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ") - if len(authHeader) == 2 { - return authHeader[1] - } - - // Get cookie from cookie - cookie, err := r.Cookie("jwt") //TODO: change cookie name - - if err == nil && len(cookie.Value) != 0 { - return cookie.Value - } - - return "" + return []byte("TODO"), nil } //setLoginCookie if provieded a valid JWT in the body then set a httpOnly cookie with the token @@ -212,7 +196,7 @@ func setLoginCookie(rw http.ResponseWriter, r *http.Request) { tokenString := string(body) - token, err := parseJWT(tokenString) + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, jwtKeyFunc) if err != nil { rw.WriteHeader(http.StatusInternalServerError) @@ -224,7 +208,7 @@ func setLoginCookie(rw http.ResponseWriter, r *http.Request) { return } - claims, ok := token.Claims.(jwt.MapClaims) + claims, ok := token.Claims.(*JWTClaims) if !ok { rw.WriteHeader(http.StatusInternalServerError) @@ -237,7 +221,7 @@ func setLoginCookie(rw http.ResponseWriter, r *http.Request) { HttpOnly: true, SameSite: http.SameSiteStrictMode, Path: "/api", - Expires: time.Unix(int64(claims["exp"].(float64)), 0), + Expires: time.Unix(claims.ExpiresAt, 0), } http.SetCookie(rw, cookie) @@ -260,3 +244,44 @@ func logout(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNoContent) } + +func refreshToken(rw http.ResponseWriter, r *http.Request) { + if is, _ := isAuth(r.Context()); !is { + rw.WriteHeader(http.StatusUnauthorized) + return + } + + oldToken, ok := r.Context().Value("jwt").(*jwt.Token) + + if !ok { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + claims, ok := oldToken.Claims.(*JWTClaims) + + if !ok { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + token := createJWT(claims) + + tokenString, err := token.SignedString([]byte("TODO")) + + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + + cookie := &http.Cookie{ + Name: "jwt", + Value: tokenString, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Path: "/api", + Expires: time.Unix(int64(claims.ExpiresAt), 0), + } + + http.SetCookie(rw, cookie) +} diff --git a/internal/mutations.go b/internal/mutations.go index 7d483b3..c88d41f 100644 --- a/internal/mutations.go +++ b/internal/mutations.go @@ -4,9 +4,7 @@ import ( "context" "fmt" "strings" - "time" - "github.com/golang-jwt/jwt" "github.com/graph-gophers/dataloader" "github.com/minio/minio-go/v7" ) @@ -202,12 +200,7 @@ func login(ctx context.Context, username, password string) (LoginResult, error) }, nil } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, JWTClaims{ - StandardClaims: jwt.StandardClaims{ - Subject: username, - ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), - }, - }) + token := createJWT(createClaims(username)) tokenString, err := token.SignedString([]byte("TODO"))