From faed343eda2e3dd25af9115379caddf3db795f44 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Wed, 22 Mar 2023 21:52:25 -0800 Subject: [PATCH] fix: cookie-auth-issues (#365) * fix session clearing on error * use singleton context to manage user state * implement remember-me functionality * fix errors * fix more errors --- .github/workflows/partial-backend.yaml | 2 +- .github/workflows/partial-frontend.yaml | 2 +- .github/workflows/partial-publish.yaml | 2 +- backend/app/api/demo.go | 4 +- backend/app/api/handlers/v1/v1_ctrl_auth.go | 9 ++- backend/app/api/static/docs/docs.go | 23 ++++++++ backend/app/api/static/docs/swagger.json | 23 ++++++++ backend/app/api/static/docs/swagger.yaml | 15 +++++ backend/go.sum | 4 -- backend/internal/core/services/main_test.go | 4 -- .../internal/core/services/service_user.go | 18 ++++-- backend/internal/data/repo/main_test.go | 4 -- backend/pkgs/faker/random.go | 1 - docs/docs/api/openapi-2.0.json | 23 ++++++++ frontend/components/Form/Password.vue | 4 +- frontend/composables/use-api.ts | 1 + frontend/composables/use-auth-context.ts | 57 +++++++++++-------- frontend/layouts/default.vue | 9 +-- frontend/lib/api/public.ts | 12 ++-- frontend/lib/api/types/data-contracts.ts | 6 ++ frontend/lib/api/user.ts | 15 ----- frontend/middleware/auth.ts | 6 +- frontend/pages/index.vue | 15 ++++- frontend/pages/profile.vue | 5 +- 24 files changed, 175 insertions(+), 89 deletions(-) diff --git a/.github/workflows/partial-backend.yaml b/.github/workflows/partial-backend.yaml index ab11ec3..7753b8d 100644 --- a/.github/workflows/partial-backend.yaml +++ b/.github/workflows/partial-backend.yaml @@ -12,7 +12,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: "1.20" - name: Install Task uses: arduino/setup-task@v1 diff --git a/.github/workflows/partial-frontend.yaml b/.github/workflows/partial-frontend.yaml index 7dc06b4..8880c7f 100644 --- a/.github/workflows/partial-frontend.yaml +++ b/.github/workflows/partial-frontend.yaml @@ -46,7 +46,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: "1.20" - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/partial-publish.yaml b/.github/workflows/partial-publish.yaml index 8663d40..4b80f19 100644 --- a/.github/workflows/partial-publish.yaml +++ b/.github/workflows/partial-publish.yaml @@ -24,7 +24,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: "1.20" - name: Set up QEMU id: qemu diff --git a/backend/app/api/demo.go b/backend/app/api/demo.go index fa83e13..5b8f7d6 100644 --- a/backend/app/api/demo.go +++ b/backend/app/api/demo.go @@ -25,7 +25,7 @@ func (a *app) SetupDemo() { } // First check if we've already setup a demo user and skip if so - _, err := a.services.User.Login(context.Background(), registration.Email, registration.Password) + _, err := a.services.User.Login(context.Background(), registration.Email, registration.Password, false) if err == nil { return } @@ -36,7 +36,7 @@ func (a *app) SetupDemo() { log.Fatal().Msg("Failed to setup demo") } - token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password) + token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password, false) self, _ := a.services.User.GetSelf(context.Background(), token.Raw) _, err = a.services.Items.CsvImport(context.Background(), self.GroupID, strings.NewReader(csvText)) diff --git a/backend/app/api/handlers/v1/v1_ctrl_auth.go b/backend/app/api/handlers/v1/v1_ctrl_auth.go index f6b723b..936f38e 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_auth.go +++ b/backend/app/api/handlers/v1/v1_ctrl_auth.go @@ -21,8 +21,9 @@ type ( } LoginForm struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username"` + Password string `json:"password"` + StayLoggedIn bool `json:"stayLoggedIn"` } ) @@ -34,6 +35,7 @@ type ( // @Accept application/json // @Param username formData string false "string" example(admin@admin.com) // @Param password formData string false "string" example(admin) +// @Param payload body LoginForm true "Login Data" // @Produce json // @Success 200 {object} TokenResponse // @Router /v1/users/login [POST] @@ -50,6 +52,7 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc { loginForm.Username = r.PostFormValue("username") loginForm.Password = r.PostFormValue("password") + loginForm.StayLoggedIn = r.PostFormValue("stayLoggedIn") == "true" case "application/json": err := server.Decode(r, loginForm) if err != nil { @@ -73,7 +76,7 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc { ) } - newToken, err := ctrl.svc.User.Login(r.Context(), strings.ToLower(loginForm.Username), loginForm.Password) + newToken, err := ctrl.svc.User.Login(r.Context(), strings.ToLower(loginForm.Username), loginForm.Password, loginForm.StayLoggedIn) if err != nil { return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError) } diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 8841610..03d0ab1 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -1575,6 +1575,15 @@ const docTemplate = `{ "description": "string", "name": "password", "in": "formData" + }, + { + "description": "Login Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.LoginForm" + } } ], "responses": { @@ -2761,6 +2770,20 @@ const docTemplate = `{ } } }, + "v1.LoginForm": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "stayLoggedIn": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, "v1.TokenResponse": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 0eaa5f4..8c6a248 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -1567,6 +1567,15 @@ "description": "string", "name": "password", "in": "formData" + }, + { + "description": "Login Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.LoginForm" + } } ], "responses": { @@ -2753,6 +2762,20 @@ } } }, + "v1.LoginForm": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "stayLoggedIn": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, "v1.TokenResponse": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index 5205c69..c458fa6 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -676,6 +676,15 @@ definitions: token: type: string type: object + v1.LoginForm: + properties: + password: + type: string + stayLoggedIn: + type: boolean + username: + type: string + type: object v1.TokenResponse: properties: attachmentToken: @@ -1642,6 +1651,12 @@ paths: in: formData name: password type: string + - description: Login Data + in: body + name: payload + required: true + schema: + $ref: '#/definitions/v1.LoginForm' produces: - application/json responses: diff --git a/backend/go.sum b/backend/go.sum index 5544f85..3fc6253 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -503,7 +503,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -529,7 +528,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -602,10 +600,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/backend/internal/core/services/main_test.go b/backend/internal/core/services/main_test.go index e1f7282..c79bfcf 100644 --- a/backend/internal/core/services/main_test.go +++ b/backend/internal/core/services/main_test.go @@ -3,10 +3,8 @@ package services import ( "context" "log" - "math/rand" "os" "testing" - "time" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/repo" @@ -49,8 +47,6 @@ func bootstrap() { } func TestMain(m *testing.M) { - rand.Seed(int64(time.Now().Unix())) - client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") if err != nil { log.Fatalf("failed opening connection to sqlite: %v", err) diff --git a/backend/internal/core/services/service_user.go b/backend/internal/core/services/service_user.go index b654029..52da946 100644 --- a/backend/internal/core/services/service_user.go +++ b/backend/internal/core/services/service_user.go @@ -140,12 +140,18 @@ func (svc *UserService) UpdateSelf(ctx context.Context, ID uuid.UUID, data repo. // ============================================================================ // User Authentication -func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID) (UserAuthTokenDetail, error) { +func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID, extendedSession bool) (UserAuthTokenDetail, error) { attachmentToken := hasher.GenerateToken() + + expiresAt := time.Now().Add(oneWeek) + if extendedSession { + expiresAt = time.Now().Add(oneWeek * 4) + } + attachmentData := repo.UserAuthTokenCreate{ UserID: userId, TokenHash: attachmentToken.Hash, - ExpiresAt: time.Now().Add(oneWeek), + ExpiresAt: expiresAt, } _, err := svc.repos.AuthTokens.CreateToken(ctx, attachmentData, authroles.RoleAttachments) @@ -157,7 +163,7 @@ func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID data := repo.UserAuthTokenCreate{ UserID: userId, TokenHash: userToken.Hash, - ExpiresAt: time.Now().Add(oneWeek), + ExpiresAt: expiresAt, } created, err := svc.repos.AuthTokens.CreateToken(ctx, data, authroles.RoleUser) @@ -172,7 +178,7 @@ func (svc *UserService) createSessionToken(ctx context.Context, userId uuid.UUID }, nil } -func (svc *UserService) Login(ctx context.Context, username, password string) (UserAuthTokenDetail, error) { +func (svc *UserService) Login(ctx context.Context, username, password string, extendedSession bool) (UserAuthTokenDetail, error) { usr, err := svc.repos.Users.GetOneEmail(ctx, username) if err != nil { // SECURITY: Perform hash to ensure response times are the same @@ -184,7 +190,7 @@ func (svc *UserService) Login(ctx context.Context, username, password string) (U return UserAuthTokenDetail{}, ErrorInvalidLogin } - return svc.createSessionToken(ctx, usr.ID) + return svc.createSessionToken(ctx, usr.ID, extendedSession) } func (svc *UserService) Logout(ctx context.Context, token string) error { @@ -201,7 +207,7 @@ func (svc *UserService) RenewToken(ctx context.Context, token string) (UserAuthT return UserAuthTokenDetail{}, ErrorInvalidToken } - return svc.createSessionToken(ctx, dbToken.ID) + return svc.createSessionToken(ctx, dbToken.ID, false) } // DeleteSelf deletes the user that is currently logged based of the provided UUID diff --git a/backend/internal/data/repo/main_test.go b/backend/internal/data/repo/main_test.go index 221fbd5..cfb1630 100644 --- a/backend/internal/data/repo/main_test.go +++ b/backend/internal/data/repo/main_test.go @@ -3,10 +3,8 @@ package repo import ( "context" "log" - "math/rand" "os" "testing" - "time" "github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/pkgs/faker" @@ -40,8 +38,6 @@ func bootstrap() { } func TestMain(m *testing.M) { - rand.Seed(int64(time.Now().Unix())) - client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") if err != nil { log.Fatalf("failed opening connection to sqlite: %v", err) diff --git a/backend/pkgs/faker/random.go b/backend/pkgs/faker/random.go index e7b51b9..2cb3b6a 100644 --- a/backend/pkgs/faker/random.go +++ b/backend/pkgs/faker/random.go @@ -10,7 +10,6 @@ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") type Faker struct{} func NewFaker() *Faker { - rand.Seed(time.Now().UnixNano()) return &Faker{} } diff --git a/docs/docs/api/openapi-2.0.json b/docs/docs/api/openapi-2.0.json index 0eaa5f4..8c6a248 100644 --- a/docs/docs/api/openapi-2.0.json +++ b/docs/docs/api/openapi-2.0.json @@ -1567,6 +1567,15 @@ "description": "string", "name": "password", "in": "formData" + }, + { + "description": "Login Data", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.LoginForm" + } } ], "responses": { @@ -2753,6 +2762,20 @@ } } }, + "v1.LoginForm": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "stayLoggedIn": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, "v1.TokenResponse": { "type": "object", "properties": { diff --git a/frontend/components/Form/Password.vue b/frontend/components/Form/Password.vue index 1769d2c..91b59c8 100644 --- a/frontend/components/Form/Password.vue +++ b/frontend/components/Form/Password.vue @@ -1,9 +1,9 @@ -
-
diff --git a/frontend/pages/profile.vue b/frontend/pages/profile.vue index 934115e..d597e92 100644 --- a/frontend/pages/profile.vue +++ b/frontend/pages/profile.vue @@ -82,14 +82,15 @@ const auth = useAuthContext(); const details = computed(() => { + console.log(auth.user); return [ { name: "Name", - text: auth.self?.name || "Unknown", + text: auth.user?.name || "Unknown", }, { name: "Email", - text: auth.self?.email || "Unknown", + text: auth.user?.email || "Unknown", }, ] as Detail[]; });