Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit 1365bdf

Browse files
authored
refactor: rewrite to cookie based auth (#578)
* rewrite to cookie based auth * remove interceptor
1 parent 2cd3c15 commit 1365bdf

File tree

8 files changed

+155
-71
lines changed

8 files changed

+155
-71
lines changed

backend/app/api/handlers/v1/controller.go

+7
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ func WithRegistration(allowRegistration bool) func(*V1Controller) {
4949
}
5050
}
5151

52+
func WithSecureCookies(secure bool) func(*V1Controller) {
53+
return func(ctrl *V1Controller) {
54+
ctrl.cookieSecure = secure
55+
}
56+
}
57+
5258
type V1Controller struct {
59+
cookieSecure bool
5360
repo *repo.AllRepos
5461
svc *services.AllServices
5562
maxUploadSize int64

backend/app/api/handlers/v1/v1_ctrl_auth.go

+105
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package v1
33
import (
44
"errors"
55
"net/http"
6+
"strconv"
67
"strings"
78
"time"
89

@@ -13,6 +14,12 @@ import (
1314
"github.com/rs/zerolog/log"
1415
)
1516

17+
const (
18+
cookieNameToken = "hb.auth.token"
19+
cookieNameRemember = "hb.auth.remember"
20+
cookieNameSession = "hb.auth.session"
21+
)
22+
1623
type (
1724
TokenResponse struct {
1825
Token string `json:"token"`
@@ -27,6 +34,30 @@ type (
2734
}
2835
)
2936

37+
type CookieContents struct {
38+
Token string
39+
ExpiresAt time.Time
40+
Remember bool
41+
}
42+
43+
func GetCookies(r *http.Request) (*CookieContents, error) {
44+
cookie, err := r.Cookie(cookieNameToken)
45+
if err != nil {
46+
return nil, errors.New("authorization cookie is required")
47+
}
48+
49+
rememberCookie, err := r.Cookie(cookieNameRemember)
50+
if err != nil {
51+
return nil, errors.New("remember cookie is required")
52+
}
53+
54+
return &CookieContents{
55+
Token: cookie.Value,
56+
ExpiresAt: cookie.Expires,
57+
Remember: rememberCookie.Value == "true",
58+
}, nil
59+
}
60+
3061
// HandleAuthLogin godoc
3162
//
3263
// @Summary User Login
@@ -81,6 +112,7 @@ func (ctrl *V1Controller) HandleAuthLogin() errchain.HandlerFunc {
81112
return validate.NewRequestError(errors.New("authentication failed"), http.StatusInternalServerError)
82113
}
83114

115+
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, loginForm.StayLoggedIn)
84116
return server.JSON(w, http.StatusOK, TokenResponse{
85117
Token: "Bearer " + newToken.Raw,
86118
ExpiresAt: newToken.ExpiresAt,
@@ -108,6 +140,7 @@ func (ctrl *V1Controller) HandleAuthLogout() errchain.HandlerFunc {
108140
return validate.NewRequestError(err, http.StatusInternalServerError)
109141
}
110142

143+
ctrl.unsetCookies(w, noPort(r.Host))
111144
return server.JSON(w, http.StatusNoContent, nil)
112145
}
113146
}
@@ -133,6 +166,78 @@ func (ctrl *V1Controller) HandleAuthRefresh() errchain.HandlerFunc {
133166
return validate.NewUnauthorizedError()
134167
}
135168

169+
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, false)
136170
return server.JSON(w, http.StatusOK, newToken)
137171
}
138172
}
173+
174+
func noPort(host string) string {
175+
return strings.Split(host, ":")[0]
176+
}
177+
178+
func (ctrl *V1Controller) setCookies(w http.ResponseWriter, domain, token string, expires time.Time, remember bool) {
179+
http.SetCookie(w, &http.Cookie{
180+
Name: cookieNameRemember,
181+
Value: strconv.FormatBool(remember),
182+
Expires: expires,
183+
Domain: domain,
184+
Secure: ctrl.cookieSecure,
185+
HttpOnly: true,
186+
Path: "/",
187+
})
188+
189+
// Set HTTP only cookie
190+
http.SetCookie(w, &http.Cookie{
191+
Name: cookieNameToken,
192+
Value: token,
193+
Expires: expires,
194+
Domain: domain,
195+
Secure: ctrl.cookieSecure,
196+
HttpOnly: true,
197+
Path: "/",
198+
})
199+
200+
// Set Fake Session cookie
201+
http.SetCookie(w, &http.Cookie{
202+
Name: cookieNameSession,
203+
Value: "true",
204+
Expires: expires,
205+
Domain: domain,
206+
Secure: ctrl.cookieSecure,
207+
HttpOnly: false,
208+
Path: "/",
209+
})
210+
}
211+
212+
func (ctrl *V1Controller) unsetCookies(w http.ResponseWriter, domain string) {
213+
http.SetCookie(w, &http.Cookie{
214+
Name: cookieNameToken,
215+
Value: "",
216+
Expires: time.Unix(0, 0),
217+
Domain: domain,
218+
Secure: ctrl.cookieSecure,
219+
HttpOnly: true,
220+
Path: "/",
221+
})
222+
223+
http.SetCookie(w, &http.Cookie{
224+
Name: cookieNameRemember,
225+
Value: "false",
226+
Expires: time.Unix(0, 0),
227+
Domain: domain,
228+
Secure: ctrl.cookieSecure,
229+
HttpOnly: true,
230+
Path: "/",
231+
})
232+
233+
// Set Fake Session cookie
234+
http.SetCookie(w, &http.Cookie{
235+
Name: cookieNameSession,
236+
Value: "false",
237+
Expires: time.Unix(0, 0),
238+
Domain: domain,
239+
Secure: ctrl.cookieSecure,
240+
HttpOnly: false,
241+
Path: "/",
242+
})
243+
}

backend/app/api/middleware.go

+21-25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/url"
88
"strings"
99

10+
v1 "github.com/hay-kot/homebox/backend/app/api/handlers/v1"
1011
"github.com/hay-kot/homebox/backend/internal/core/services"
1112
"github.com/hay-kot/homebox/backend/internal/sys/validate"
1213
"github.com/hay-kot/httpkit/errchain"
@@ -94,42 +95,37 @@ func getQuery(r *http.Request) (string, error) {
9495
return token, nil
9596
}
9697

97-
func getCookie(r *http.Request) (string, error) {
98-
cookie, err := r.Cookie("hb.auth.token")
99-
if err != nil {
100-
return "", errors.New("access_token cookie is required")
101-
}
102-
103-
token, err := url.QueryUnescape(cookie.Value)
104-
if err != nil {
105-
return "", errors.New("access_token cookie is required")
106-
}
107-
108-
return token, nil
109-
}
110-
11198
// mwAuthToken is a middleware that will check the database for a stateful token
11299
// and attach it's user to the request context, or return an appropriate error.
113100
// Authorization support is by token via Headers or Query Parameter
114101
//
115102
// Example:
116103
// - header = "Bearer 1234567890"
117104
// - query = "?access_token=1234567890"
118-
// - cookie = hb.auth.token = 1234567890
119105
func (a *app) mwAuthToken(next errchain.Handler) errchain.Handler {
120106
return errchain.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
121-
keyFuncs := [...]KeyFunc{
122-
getBearer,
123-
getCookie,
124-
getQuery,
107+
var requestToken string
108+
109+
// We ignore the error to allow the next strategy to be attempted
110+
{
111+
cookies, _ := v1.GetCookies(r)
112+
if cookies != nil {
113+
requestToken = cookies.Token
114+
}
125115
}
126116

127-
var requestToken string
128-
for _, keyFunc := range keyFuncs {
129-
token, err := keyFunc(r)
130-
if err == nil {
131-
requestToken = token
132-
break
117+
if requestToken == "" {
118+
keyFuncs := [...]KeyFunc{
119+
getBearer,
120+
getQuery,
121+
}
122+
123+
for _, keyFunc := range keyFuncs {
124+
token, err := keyFunc(r)
125+
if err == nil {
126+
requestToken = token
127+
break
128+
}
133129
}
134130
}
135131

frontend/composables/use-api.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ export function usePublicApi(): PublicApi {
3030
export function useUserApi(): UserClient {
3131
const authCtx = useAuthContext();
3232

33-
const requests = new Requests("", () => authCtx.token || "", {});
33+
const requests = new Requests("", "", {});
3434
requests.addResponseInterceptor(logger);
3535
requests.addResponseInterceptor(r => {
3636
if (r.status === 401) {
3737
console.error("unauthorized request, invalidating session");
3838
authCtx.invalidateSession();
39+
if (window.location.pathname !== "/") {
40+
window.location.href = "/";
41+
}
3942
}
4043
});
4144

frontend/composables/use-auth-context.ts

+8-42
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,14 @@ import { UserOut } from "~~/lib/api/types/data-contracts";
44
import { UserClient } from "~~/lib/api/user";
55

66
export interface IAuthContext {
7-
get token(): string | null;
8-
get expiresAt(): string | null;
7+
get token(): boolean | null;
98
get attachmentToken(): string | null;
109

1110
/**
1211
* The current user object for the session. This is undefined if the session is not authorized.
1312
*/
1413
user?: UserOut;
1514

16-
/**
17-
* Returns true if the session is expired.
18-
*/
19-
isExpired(): boolean;
20-
2115
/**
2216
* Returns true if the session is authorized.
2317
*/
@@ -43,89 +37,61 @@ class AuthContext implements IAuthContext {
4337
// eslint-disable-next-line no-use-before-define
4438
private static _instance?: AuthContext;
4539

46-
private static readonly cookieTokenKey = "hb.auth.token";
47-
private static readonly cookieExpiresAtKey = "hb.auth.expires_at";
40+
private static readonly cookieTokenKey = "hb.auth.session";
4841
private static readonly cookieAttachmentTokenKey = "hb.auth.attachment_token";
4942

5043
user?: UserOut;
5144
private _token: CookieRef<string | null>;
52-
private _expiresAt: CookieRef<string | null>;
5345
private _attachmentToken: CookieRef<string | null>;
5446

5547
get token() {
56-
return this._token.value;
57-
}
58-
59-
get expiresAt() {
60-
return this._expiresAt.value;
48+
return this._token.value === "true";
6149
}
6250

6351
get attachmentToken() {
6452
return this._attachmentToken.value;
6553
}
6654

67-
private constructor(token: string, expiresAt: string, attachmentToken: string) {
55+
private constructor(token: string, attachmentToken: string) {
6856
this._token = useCookie(token);
69-
this._expiresAt = useCookie(expiresAt);
7057
this._attachmentToken = useCookie(attachmentToken);
7158
}
7259

7360
static get instance() {
7461
if (!this._instance) {
75-
this._instance = new AuthContext(
76-
AuthContext.cookieTokenKey,
77-
AuthContext.cookieExpiresAtKey,
78-
AuthContext.cookieAttachmentTokenKey
79-
);
62+
this._instance = new AuthContext(AuthContext.cookieTokenKey, AuthContext.cookieAttachmentTokenKey);
8063
}
8164

8265
return this._instance;
8366
}
8467

8568
isExpired() {
86-
const expiresAt = this.expiresAt;
87-
if (expiresAt === null) {
88-
return true;
89-
}
90-
91-
const expiresAtDate = new Date(expiresAt);
92-
const now = new Date();
93-
94-
return now.getTime() > expiresAtDate.getTime();
69+
return this.token;
9570
}
9671

9772
isAuthorized() {
98-
return !!this._token.value && !this.isExpired();
73+
return !this.isExpired();
9974
}
10075

10176
invalidateSession() {
10277
this.user = undefined;
10378

10479
// Delete the cookies
10580
this._token.value = null;
106-
this._expiresAt.value = null;
10781
this._attachmentToken.value = null;
10882

10983
console.log("Session invalidated");
110-
window.location.href = "/";
11184
}
11285

11386
async login(api: PublicApi, email: string, password: string, stayLoggedIn: boolean) {
11487
const r = await api.login(email, password, stayLoggedIn);
11588

11689
if (!r.error) {
11790
const expiresAt = new Date(r.data.expiresAt);
118-
this._token = useCookie(AuthContext.cookieTokenKey, {
119-
expires: expiresAt,
120-
});
121-
this._expiresAt = useCookie(AuthContext.cookieExpiresAtKey, {
122-
expires: expiresAt,
123-
});
91+
this._token = useCookie(AuthContext.cookieTokenKey);
12492
this._attachmentToken = useCookie(AuthContext.cookieAttachmentTokenKey, {
12593
expires: expiresAt,
12694
});
127-
this._token.value = r.data.token;
128-
this._expiresAt.value = r.data.expiresAt as string;
12995
this._attachmentToken.value = r.data.attachmentToken;
13096
}
13197

frontend/layouts/default.vue

+1
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,6 @@
199199
200200
async function logout() {
201201
await authCtx.logout(api);
202+
navigateTo("/");
202203
}
203204
</script>

frontend/middleware/auth.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default defineNuxtRouteMiddleware(async () => {
77
}
88

99
if (!ctx.user) {
10+
console.log("Fetching user data");
1011
const { data, error } = await api.user.self();
1112
if (error) {
1213
return navigateTo("/");

0 commit comments

Comments
 (0)