branch bow-v2-go updated (96a2e98 -> e366ae7)
This is an automated email from the git hooks/post-receive script. New change to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git from 96a2e98 ajout de dossier pkg pour mettre les modules (go rules) new e366ae7 ajout de 2 tests pour modele plus tard ajout de l'auth jwt The 1 revisions listed above as "new" are entirely new to this repository and will be described in separate emails. The revisions listed as "adds" were already present in the repository and have only been added to this reference. Detailed log of new commits: commit e366ae7b41e3e13f6ca541e3d65dddff2dcb7633 Author: Benjamin <poussin@codelutin.com> Date: Wed Apr 8 23:55:10 2020 +0200 ajout de 2 tests pour modele plus tard ajout de l'auth jwt Summary of changes: doc/implementation.md | 25 ++++++++++++++ go.mod | 1 + go.sum | 2 ++ pkg/http/router.go | 74 ++++++++++++++++++++++++++++++++++++++++ pkg/http/systemResource.go | 2 +- pkg/http/systemResource_test.go | 39 +++++++++++++++++++++ pkg/http/userResource.go | 63 +++++++++++++++++++++++++++++++++- pkg/repository/userRepository.go | 55 ++++++++++++++++++++++------- pkg/utils/const.go | 18 ++++++++++ pkg/utils/jwt.go | 55 +++++++++++++++++++++++++++++ pkg/utils/uuid_test.go | 17 +++++++++ 11 files changed, 337 insertions(+), 14 deletions(-) create mode 100644 pkg/http/systemResource_test.go create mode 100644 pkg/utils/const.go create mode 100644 pkg/utils/jwt.go create mode 100644 pkg/utils/uuid_test.go -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.
This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit e366ae7b41e3e13f6ca541e3d65dddff2dcb7633 Author: Benjamin <poussin@codelutin.com> Date: Wed Apr 8 23:55:10 2020 +0200 ajout de 2 tests pour modele plus tard ajout de l'auth jwt --- doc/implementation.md | 25 ++++++++++++++ go.mod | 1 + go.sum | 2 ++ pkg/http/router.go | 74 ++++++++++++++++++++++++++++++++++++++++ pkg/http/systemResource.go | 2 +- pkg/http/systemResource_test.go | 39 +++++++++++++++++++++ pkg/http/userResource.go | 63 +++++++++++++++++++++++++++++++++- pkg/repository/userRepository.go | 55 ++++++++++++++++++++++------- pkg/utils/const.go | 18 ++++++++++ pkg/utils/jwt.go | 55 +++++++++++++++++++++++++++++ pkg/utils/uuid_test.go | 17 +++++++++ 11 files changed, 337 insertions(+), 14 deletions(-) diff --git a/doc/implementation.md b/doc/implementation.md index 6d7ade8..b8f3fcb 100644 --- a/doc/implementation.md +++ b/doc/implementation.md @@ -1,3 +1,28 @@ +TODO: table d'historique d'authentification + +== Creation d'un compte + +La création d'un compte, ne fait qu'envoyer un mail avec un lien contenant +un jeton jwt crypté ensuite avec "crypto/aes". Si la personne clique réellement +sur le lien cela valide la création du compte. + +Le jeton contient: +- le mail +- le mot de passe + +== Droit dans Postgresql + +L'utilisateur ayant créé la base n'est pas utilisé pour le login applicatif + +Il y a un utilisateur (nobody) qui n'a droit sur aucun objet met qui peut endoser le role +de tous les autres. C'est cet utilisateur qui est utilisé dans l'applicatif. +Si la personne qui arrive n'a pas de token (pas authentifiée) alors elle reste +avec cet utilisateur sans droit. + +Les seuls droits qu'il a est: +- création d'un nouvelle utilisateur +- lecture des mots de passe + == Optimisation Posggresql Les tableaux (ex: text[]) utilise des index gin, il faut donc utiliser seulement diff --git a/go.mod b/go.mod index 68f24cd..3e4a88c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.chorem.org/chorem/bow go 1.14 require ( + github.com/brianvoe/sjwt v0.5.1 github.com/gorilla/mux v1.7.4 github.com/jackc/pgtype v1.3.0 github.com/jackc/pgx/v4 v4.6.0 diff --git a/go.sum b/go.sum index 1e338b6..fd87830 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/brianvoe/sjwt v0.5.1 h1:OKwnUrrVMnP81N9S5+ylgZECUEwW4Uw6W6J0FgcIZfw= +github.com/brianvoe/sjwt v0.5.1/go.mod h1:GsyrNi4zWvWAcsVGNNMULQ8SfDMmJ2ybzAyPjNQJJL8= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= diff --git a/pkg/http/router.go b/pkg/http/router.go index 5fb2ca5..9ecb3a1 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -1,10 +1,17 @@ package http import ( + "context" + "fmt" + "io" "log" "net/http" + "strings" "time" + "gitlab.chorem.org/chorem/bow/pkg/repository" + "gitlab.chorem.org/chorem/bow/pkg/utils" + "github.com/gorilla/mux" ) @@ -13,11 +20,16 @@ Start web server */ func Start(addr string) { router := mux.NewRouter() + router.Use(authentication) s := router.PathPrefix("/api/v1").Subrouter() s.HandleFunc("/system/liveness", isAlive).Methods("GET") s.HandleFunc("/users", createUser).Methods("POST") + s.HandleFunc("/users/auth", createAuth).Methods("POST") + s.HandleFunc("/users/{id}", getUser).Methods("GET") s.HandleFunc("/users/{id}", deleteUser).Methods("DELETE") + s.HandleFunc("/users/{id}/auth", createAuth).Methods("POST") + s.HandleFunc("/users/{id}/auth", deleteAuth).Methods("DELETE") s.HandleFunc("/users/{id}/password", updateUserPassword).Methods("PUT") s.HandleFunc("/users/{id}/token", addUserToken).Methods("POST") s.HandleFunc("/users/{id}/unconfirmedemails", addUserUnconfirmedEmail).Methods("POST") @@ -44,3 +56,65 @@ func Start(addr string) { log.Fatal(srv.ListenAndServe()) } + +func authentication(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + isAppToken := false + + // 1 as query param + query := r.URL.Query() + token := query.Get(utils.TokenName) + + // 2 as bow header + if token == "" { + token = r.Header.Get(utils.TokenHeaderName) + } else { + isAppToken = true + } + + // 3 as Authorization header + if token == "" { + tokenBearer := r.Header.Get("Authorization") + if tokenBearer != "" { + splitted := strings.Split(tokenBearer, " ") //The token normally comes in format `Bearer {token-body}`, we check if the retrieved token matched this requirement + if len(splitted) != 2 { + w.WriteHeader(http.StatusForbidden) + w.Header().Add("Content-Type", "application/json") + io.WriteString(w, `{"message": "Invalid/Malformed auth token header Authorization"}`) + return + } + token = splitted[1] + } + } + + // 4 as cookie + if token == "" { + cookie, err := r.Cookie(utils.TokenName) + if err == nil { + token = cookie.Value + } + } + + userID := utils.JwtVerify(token) + + if userID == "" && isAppToken { + tmp, err := repository.UserIDFromToken(token) + if err != nil { + http.Error(w, fmt.Sprintf("%s", err), 500) + return + } + userID = tmp + } + + if userID == "" { + userID = "nobody" + } + + log.Printf("User is '%s'\n", userID) + + ctx := context.WithValue(r.Context(), utils.UserID, userID) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + }) + +} diff --git a/pkg/http/systemResource.go b/pkg/http/systemResource.go index c8afefd..482e97f 100644 --- a/pkg/http/systemResource.go +++ b/pkg/http/systemResource.go @@ -8,5 +8,5 @@ import ( func isAlive(w http.ResponseWriter, r *http.Request) { log.Println("http liveness") - json.NewEncoder(w).Encode(map[string]bool{"ok": true}) + json.NewEncoder(w).Encode(map[string]bool{"alive": true}) } diff --git a/pkg/http/systemResource_test.go b/pkg/http/systemResource_test.go new file mode 100644 index 0000000..571a7b1 --- /dev/null +++ b/pkg/http/systemResource_test.go @@ -0,0 +1,39 @@ +package http + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/api/v1/system/liveness", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(isAlive) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive":true}` + result := strings.TrimSpace(rr.Body.String()) + if result != expected { + t.Errorf("handler returned unexpected body: got '%v' want '%v'", + result, expected) + } +} diff --git a/pkg/http/userResource.go b/pkg/http/userResource.go index 851862e..d8fdb00 100644 --- a/pkg/http/userResource.go +++ b/pkg/http/userResource.go @@ -3,14 +3,75 @@ package http import ( "encoding/json" "fmt" + "io" "log" "net/http" "github.com/gorilla/mux" "gitlab.chorem.org/chorem/bow/pkg/model" "gitlab.chorem.org/chorem/bow/pkg/repository" + "gitlab.chorem.org/chorem/bow/pkg/utils" ) +/* +DeleteAuth remove cookie authentication +*/ +func deleteAuth(w http.ResponseWriter, r *http.Request) { + cookie := http.Cookie{Name: utils.TokenName, Value: "", HttpOnly: true, MaxAge: 0} + http.SetCookie(w, &cookie) +} + +/* +CreateAuth create JWT token and set header, cookie, body +body: {"token": "xxxx"} +*/ +func createAuth(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + + var data map[string]string + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + http.Error(w, fmt.Sprintf("%s", err), 400) + return + } + + if checkLogin(id, data["email"], data["password"]) { + token := utils.JwtGenerate(id) + w.Header().Add(utils.TokenHeaderName, token) + + cookie := http.Cookie{Name: utils.TokenName, Value: token, HttpOnly: true} + http.SetCookie(w, &cookie) + + w.Header().Add("Content-Type", "application/json") + io.WriteString(w, fmt.Sprintf(`{"token": "%s"}`, token)) + } else { + http.Error(w, "Bad id or password", 403) + } +} + +func checkLogin(id string, email string, password string) bool { + if id != "" && repository.CheckUserPasswordForID(id, password) { + return true + } + + _, err := repository.CheckUserPasswordForEmail(email, password) + return err == nil +} + +/* +GetUser return all information on user (info, config, auth) +*/ +func getUser(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + json, err := repository.UserJSON(id) + if err != nil { + http.Error(w, fmt.Sprintf("%s", err), 500) + } + + w.Header().Add("Content-Type", "application/json") + io.WriteString(w, json) +} + /* createUser body: {"login": "toto", "password": "xxxx"} @@ -47,7 +108,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { } /* -createUser +updateUserPassword body: {"password": "xxxx", "oldPassword": "yyyy"} */ func updateUserPassword(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/repository/userRepository.go b/pkg/repository/userRepository.go index 1622125..ce45986 100644 --- a/pkg/repository/userRepository.go +++ b/pkg/repository/userRepository.go @@ -14,11 +14,19 @@ import ( ) /* -UserJSON retourne l'utilisateur au format json +UserJSON return user in json format +all field are send except: +- password +- email confirmation token */ func UserJSON(id string) (string, error) { var pgjson pgtype.JSON - row := db.QueryRow(context.Background(), `WITH __all AS (select * from bowuser where id=$1) SELECT json_agg(__all.*) as j FROM __all`, id) + allFields := "id, creationdate, updatedate, login, tokens, emails, authenticationinfo, autoscreenshot, autofavicon, maxtagincloud, maxresult, actions" + row := db.QueryRow(context.Background(), fmt.Sprintf(` + WITH tmp AS (select %[1]s, (jsonb_array_elements(unconfirmedemails)::jsonb)->'email' as unconfirmedemails from bowuser where id=$1), + __all AS (select %[1]s, array_agg(unconfirmedemails) as unconfirmedemails from tmp group by %[1]s) + SELECT json_agg(__all.*) as j + FROM __all`, allFields), id) err := row.Scan(&pgjson) if err != nil { return "", err @@ -31,9 +39,39 @@ func UserJSON(id string) (string, error) { } /* -UserJSON retourne l'id de l'utilisateur (string) si le mot de passe est le bon et que l'utilisateur est retrouve +UserIdFromToken get user id by application token */ -func checkPassword(loginOrEmail string, password string) (string, error) { +func UserIDFromToken(token string) (string, error) { + + tokenJSON := fmt.Sprintf(`{"token": "%s"}`, token) + var id string + row := db.QueryRow(context.Background(), ` + select id from bowuser b + where exists (select * from jsonb_array_elements(tokens) as x + where x @> $1);`, tokenJSON) + err := row.Scan(&id) + + return id, err +} + +/* +CheckUserPasswordForID check password for id +*/ +func CheckUserPasswordForID(id string, password string) bool { + var hash string + row := db.QueryRow(context.Background(), `select password from bowuser where id=$1`, id) + err := row.Scan(&hash) + if err != nil { + return false + } + + return utils.CheckPassword(password, hash) +} + +/* +CheckUserPasswordForEmail retourne l'id de l'utilisateur (string) si le mot de passe est le bon et que l'utilisateur est retrouve +*/ +func CheckUserPasswordForEmail(loginOrEmail string, password string) (string, error) { var uuid pgtype.UUID var hash string row := db.QueryRow(context.Background(), `select id, password from bowuser where login=$1 or emails @> $2::text[]`, loginOrEmail, fmt.Sprintf(`{"%s"}`, loginOrEmail)) @@ -102,14 +140,7 @@ func DeleteUser(id string) error { UpdateUserPassword update user password, if old password match, or if force is true */ func UpdateUserPassword(id string, password string, oldPassword string, force bool) error { - var hash string - row := db.QueryRow(context.Background(), `select password from bowuser where id=$1`, id) - err := row.Scan(&hash) - if err != nil { - return err - } - - if force || utils.CheckPassword(oldPassword, hash) { + if force || CheckUserPasswordForID(id, oldPassword) { hash, err := utils.HashPassword(password) diff --git a/pkg/utils/const.go b/pkg/utils/const.go new file mode 100644 index 0000000..ce7ab40 --- /dev/null +++ b/pkg/utils/const.go @@ -0,0 +1,18 @@ +package utils + +type httpRequestConxtKey string + +/* +UserID constant pour stocker le userID dans le context de la requete http +*/ +const UserID = httpRequestConxtKey("userID") + +/* +TokenName le nom utiliser dans le cookie et le parameter de query +*/ +const TokenName = "bow-token" + +/* +TokenHeaderName le nom utiliser pour mettre dans le header de requete http (fallback Authorization) +*/ +const TokenHeaderName = "x-" + TokenName \ No newline at end of file diff --git a/pkg/utils/jwt.go b/pkg/utils/jwt.go new file mode 100644 index 0000000..4283b84 --- /dev/null +++ b/pkg/utils/jwt.go @@ -0,0 +1,55 @@ +package utils + +import ( + "github.com/brianvoe/sjwt" +) + +var secretKey []byte + +/* +JwtInit JWT secret key +*/ +func JwtInit(key []byte) { + secretKey = key +} + +/* +JwtVerify check token and if valide return user id +*/ +func JwtVerify(token string) string { + // check signature + verified := sjwt.Verify(token, secretKey) + if !verified { + return "" + } + + // read token + claims, err := sjwt.Parse(token) + if err != nil { + return "" + } + + // check date + err = claims.Validate() + if err != nil { + return "" + } + + id, err := claims.GetStr("id") + if err != nil { + return "" + } + + return id +} + +/* +JwtGenerate generate JWT token from information in parameter +*/ +func JwtGenerate(id string) string { + claims := sjwt.New() + claims.Set("id", id) + jwt := claims.Generate(secretKey) + + return jwt +} diff --git a/pkg/utils/uuid_test.go b/pkg/utils/uuid_test.go new file mode 100644 index 0000000..c36ab3b --- /dev/null +++ b/pkg/utils/uuid_test.go @@ -0,0 +1,17 @@ +package utils + +import ( + "testing" + "regexp" +) + +func TestGenUUID(t *testing.T) { + uuid, err := GenUUID() + if err != nil { + t.Error("TestGenUUID", err) + } + matched, err := regexp.MatchString(`^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$`, uuid) + if !matched || err != nil { + t.Error("TestGenUUID", matched, err) + } +} -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.
participants (1)
-
chorem.org scm