implemented simple login with JWT

This commit is contained in:
Djeeberjr 2021-09-14 15:16:37 +02:00
parent 2b25003344
commit 426531e634
5 changed files with 153 additions and 1 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.16
require ( require (
github.com/alexflint/go-arg v1.4.2 github.com/alexflint/go-arg v1.4.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/graph-gophers/dataloader v5.0.0+incompatible github.com/graph-gophers/dataloader v5.0.0+incompatible
github.com/graphql-go/graphql v0.7.9 github.com/graphql-go/graphql v0.7.9

2
go.sum
View File

@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View File

@ -7,7 +7,10 @@ import (
"mime" "mime"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/graphql-go/graphql" "github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/gqlerrors" "github.com/graphql-go/graphql/gqlerrors"
@ -17,6 +20,10 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type JWTClaims struct {
jwt.StandardClaims
}
// initHttp setup and start the http server. Blocking // initHttp setup and start the http server. Blocking
func initHttp(resolveContext context.Context, schema graphql.Schema, address string) error { func initHttp(resolveContext context.Context, schema graphql.Schema, address string) error {
r := mux.NewRouter() r := mux.NewRouter()
@ -39,7 +46,17 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
r.Use(func(h http.Handler) http.Handler { r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// TODO: handle auth
token := getTokenFromRequest(r)
if token != "" {
parsedToken, err := parseJWT(token)
if err == nil && parsedToken.Valid {
r.WithContext(context.WithValue(r.Context(), "jwt", parsedToken))
}
}
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
}) })
}) })
@ -56,6 +73,8 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
httpPostFile(resolveContext, rw, r) httpPostFile(resolveContext, rw, r)
}).Methods("POST") }).Methods("POST")
r.HandleFunc("/api/cookie", setLoginCookie).Methods("POST")
// Init the embedded static files // Init the embedded static files
initStatic(r) initStatic(r)
@ -126,3 +145,87 @@ func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request)
rw.WriteHeader(http.StatusCreated) 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")
}
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 ""
}
//setLoginCookie if provieded a valid JWT in the body then set a httpOnly cookie with the token
func setLoginCookie(rw http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
tokenString := string(body)
token, err := parseJWT(tokenString)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
if !token.Valid {
rw.WriteHeader(http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return
}
cookie := &http.Cookie{
Name: "jwt",
Value: tokenString,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/api",
Expires: time.Unix(int64(claims["exp"].(float64)), 0),
}
http.SetCookie(rw, cookie)
rw.WriteHeader(http.StatusNoContent)
}

View File

@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/golang-jwt/jwt"
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
) )
@ -189,3 +191,21 @@ func deleteDirectory(ctx context.Context, path string) error {
return nil return nil
} }
//login Checks for valid username password combination. Returns singed jwt string
func login(ctx context.Context, username, password string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, JWTClaims{
StandardClaims: jwt.StandardClaims{
Subject: username,
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
},
})
tokenString, err := token.SignedString([]byte("TODO"))
if err != nil {
return "", err
}
return tokenString, nil
}

View File

@ -183,6 +183,32 @@ func graphqlSchema() (graphql.Schema, error) {
return path, deleteDirectory(p.Context, path) return path, deleteDirectory(p.Context, path)
}, },
}, },
"login": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Args: graphql.FieldConfigArgument{
"username": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"password": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
username, ok := p.Args["username"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
password, ok := p.Args["password"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
return login(p.Context, username, password)
},
},
} }
rootQuery := graphql.ObjectConfig{ rootQuery := graphql.ObjectConfig{