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

Commit 12975ce

Browse files
authored
feat: change auth to use cookies (#301)
* frontend cookie implementation * accept cookies for authentication * remove auth store * add self attr
1 parent bd321af commit 12975ce

File tree

9 files changed

+204
-86
lines changed

9 files changed

+204
-86
lines changed

backend/app/api/middleware.go

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"net/http"
7+
"net/url"
78
"strings"
89

910
"github.com/hay-kot/homebox/backend/internal/core/services"
@@ -68,33 +69,82 @@ func (a *app) mwRoles(rm RoleMode, required ...string) server.Middleware {
6869
}
6970
}
7071

72+
type KeyFunc func(r *http.Request) (string, error)
73+
74+
func getBearer(r *http.Request) (string, error) {
75+
auth := r.Header.Get("Authorization")
76+
if auth == "" {
77+
return "", errors.New("authorization header is required")
78+
}
79+
80+
return auth, nil
81+
}
82+
83+
func getQuery(r *http.Request) (string, error) {
84+
token := r.URL.Query().Get("access_token")
85+
if token == "" {
86+
return "", errors.New("access_token query is required")
87+
}
88+
89+
token, err := url.QueryUnescape(token)
90+
if err != nil {
91+
return "", errors.New("access_token query is required")
92+
}
93+
94+
return token, nil
95+
}
96+
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+
71111
// mwAuthToken is a middleware that will check the database for a stateful token
72112
// and attach it's user to the request context, or return an appropriate error.
73113
// Authorization support is by token via Headers or Query Parameter
74114
//
75115
// Example:
76116
// - header = "Bearer 1234567890"
77117
// - query = "?access_token=1234567890"
118+
// - cookie = hb.auth.token = 1234567890
78119
func (a *app) mwAuthToken(next server.Handler) server.Handler {
79120
return server.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
80-
requestToken := r.Header.Get("Authorization")
81-
if requestToken == "" {
82-
// check for query param
83-
requestToken = r.URL.Query().Get("access_token")
84-
if requestToken == "" {
85-
return validate.NewRequestError(errors.New("Authorization header or query is required"), http.StatusUnauthorized)
121+
keyFuncs := [...]KeyFunc{
122+
getBearer,
123+
getCookie,
124+
getQuery,
125+
}
126+
127+
var requestToken string
128+
for _, keyFunc := range keyFuncs {
129+
token, err := keyFunc(r)
130+
if err == nil {
131+
requestToken = token
132+
break
86133
}
87134
}
88135

136+
if requestToken == "" {
137+
return validate.NewRequestError(errors.New("Authorization header or query is required"), http.StatusUnauthorized)
138+
}
139+
89140
requestToken = strings.TrimPrefix(requestToken, "Bearer ")
90141

91142
r = r.WithContext(context.WithValue(r.Context(), hashedToken, requestToken))
92143

93144
usr, err := a.services.User.GetSelf(r.Context(), requestToken)
94-
95145
// Check the database for the token
96146
if err != nil {
97-
return validate.NewRequestError(errors.New("Authorization header is required"), http.StatusUnauthorized)
147+
return validate.NewRequestError(errors.New("valid authorization header is required"), http.StatusUnauthorized)
98148
}
99149

100150
r = r.WithContext(services.SetUserCtx(r.Context(), &usr, requestToken))

frontend/components/App/Header.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
<script lang="ts" setup>
2-
import { useAuthStore } from "~~/stores/auth";
3-
4-
const authStore = useAuthStore();
2+
const ctx = useAuthContext();
53
const api = useUserApi();
64
75
async function logout() {
8-
const { error } = await authStore.logout(api);
6+
const { error } = await ctx.logout(api);
97
if (error) {
108
return;
119
}

frontend/composables/use-api.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { PublicApi } from "~~/lib/api/public";
22
import { UserClient } from "~~/lib/api/user";
33
import { Requests } from "~~/lib/requests";
4-
import { useAuthStore } from "~~/stores/auth";
54

65
export type Observer = {
76
handler: (r: Response, req?: RequestInit) => void;
@@ -29,19 +28,19 @@ export function usePublicApi(): PublicApi {
2928
}
3029

3130
export function useUserApi(): UserClient {
32-
const authStore = useAuthStore();
31+
const authCtx = useAuthContext();
3332

34-
const requests = new Requests("", () => authStore.token, {});
33+
const requests = new Requests("", () => authCtx.token || "", {});
3534
requests.addResponseInterceptor(logger);
3635
requests.addResponseInterceptor(r => {
3736
if (r.status === 401) {
38-
authStore.clearSession();
37+
authCtx.invalidateSession();
3938
}
4039
});
4140

4241
for (const [_, observer] of Object.entries(observers)) {
4342
requests.addResponseInterceptor(observer.handler);
4443
}
4544

46-
return new UserClient(requests, authStore.attachmentToken);
45+
return new UserClient(requests, authCtx.attachmentToken || "");
4746
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { CookieRef } from "nuxt/dist/app/composables";
2+
import { PublicApi } from "~~/lib/api/public";
3+
import { UserOut } from "~~/lib/api/types/data-contracts";
4+
import { UserClient } from "~~/lib/api/user";
5+
6+
export interface IAuthContext {
7+
self?: UserOut;
8+
get token(): string | null;
9+
get expiresAt(): string | null;
10+
get attachmentToken(): string | null;
11+
12+
/**
13+
* The current user object for the session. This is undefined if the session is not authorized.
14+
*/
15+
user?: UserOut;
16+
17+
/**
18+
* Returns true if the session is expired.
19+
*/
20+
isExpired(): boolean;
21+
22+
/**
23+
* Returns true if the session is authorized.
24+
*/
25+
isAuthorized(): boolean;
26+
27+
/**
28+
* Invalidates the session by removing the token and the expiresAt.
29+
*/
30+
invalidateSession(): void;
31+
32+
/**
33+
* Logs out the user and calls the invalidateSession method.
34+
*/
35+
logout(api: UserClient): ReturnType<UserClient["user"]["logout"]>;
36+
37+
/**
38+
* Logs in the user and sets the authorization context via cookies
39+
*/
40+
login(api: PublicApi, email: string, password: string): ReturnType<PublicApi["login"]>;
41+
}
42+
43+
class AuthContext implements IAuthContext {
44+
user?: UserOut;
45+
private _token: CookieRef<string | null>;
46+
private _expiresAt: CookieRef<string | null>;
47+
private _attachmentToken: CookieRef<string | null>;
48+
49+
get token() {
50+
return this._token.value;
51+
}
52+
53+
get expiresAt() {
54+
return this._expiresAt.value;
55+
}
56+
57+
get attachmentToken() {
58+
return this._attachmentToken.value;
59+
}
60+
61+
constructor(
62+
token: CookieRef<string | null>,
63+
expiresAt: CookieRef<string | null>,
64+
attachmentToken: CookieRef<string | null>
65+
) {
66+
this._token = token;
67+
this._expiresAt = expiresAt;
68+
this._attachmentToken = attachmentToken;
69+
}
70+
71+
isExpired() {
72+
const expiresAt = this.expiresAt;
73+
if (expiresAt === null) {
74+
return true;
75+
}
76+
77+
const expiresAtDate = new Date(expiresAt);
78+
const now = new Date();
79+
80+
return now.getTime() > expiresAtDate.getTime();
81+
}
82+
83+
isAuthorized() {
84+
return this._token.value !== null && !this.isExpired();
85+
}
86+
87+
invalidateSession() {
88+
this.user = undefined;
89+
this._token.value = null;
90+
this._expiresAt.value = null;
91+
this._attachmentToken.value = null;
92+
}
93+
94+
async login(api: PublicApi, email: string, password: string) {
95+
const r = await api.login(email, password);
96+
97+
if (!r.error) {
98+
this._token.value = r.data.token;
99+
this._expiresAt.value = r.data.expiresAt as string;
100+
this._attachmentToken.value = r.data.attachmentToken;
101+
102+
console.log({
103+
token: this._token.value,
104+
expiresAt: this._expiresAt.value,
105+
attachmentToken: this._attachmentToken.value,
106+
});
107+
}
108+
109+
return r;
110+
}
111+
112+
async logout(api: UserClient) {
113+
const r = await api.user.logout();
114+
115+
if (!r.error) {
116+
this.invalidateSession();
117+
}
118+
119+
return r;
120+
}
121+
}
122+
123+
export function useAuthContext(): IAuthContext {
124+
const tokenCookie = useCookie("hb.auth.token");
125+
const expiresAtCookie = useCookie("hb.auth.expires_at");
126+
const attachmentTokenCookie = useCookie("hb.auth.attachment_token");
127+
128+
return new AuthContext(tokenCookie, expiresAtCookie, attachmentTokenCookie);
129+
}

frontend/layouts/default.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,10 @@
9191
</template>
9292

9393
<script lang="ts" setup>
94-
import { useAuthStore } from "~~/stores/auth";
9594
import { useLabelStore } from "~~/stores/labels";
9695
import { useLocationStore } from "~~/stores/locations";
9796
98-
const username = computed(() => authStore.self?.name || "User");
97+
const username = computed(() => authCtx.self?.name || "User");
9998
10099
// Preload currency format
101100
useFormatCurrency();
@@ -223,11 +222,11 @@
223222
eventBus.off(EventTypes.InvalidStores, "stores");
224223
});
225224
226-
const authStore = useAuthStore();
225+
const authCtx = useAuthContext();
227226
const api = useUserApi();
228227
229228
async function logout() {
230-
const { error } = await authStore.logout(api);
229+
const { error } = await authCtx.logout(api);
231230
if (error) {
232231
return;
233232
}

frontend/middleware/auth.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { useAuthStore } from "~~/stores/auth";
2-
31
export default defineNuxtRouteMiddleware(async () => {
4-
const auth = useAuthStore();
2+
const ctx = useAuthContext();
53
const api = useUserApi();
64

7-
if (!auth.self) {
5+
if (!ctx.user) {
86
const { data, error } = await api.user.self();
97
if (error) {
108
navigateTo("/");
119
}
1210

13-
auth.$patch({ self: data.item });
11+
ctx.user = data.item;
1412
}
1513
});

frontend/pages/index.vue

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script setup lang="ts">
2-
import { useAuthStore } from "~~/stores/auth";
32
useHead({
43
title: "Homebox | Organize and Tag Your Stuff",
54
});
@@ -8,6 +7,8 @@
87
layout: "empty",
98
});
109
10+
const ctx = useAuthContext();
11+
1112
const api = usePublicApi();
1213
const toast = useNotifier();
1314
@@ -28,8 +29,7 @@
2829
}
2930
});
3031
31-
const authStore = useAuthStore();
32-
if (!authStore.isTokenExpired) {
32+
if (!ctx.isAuthorized()) {
3333
navigateTo("/home");
3434
}
3535
@@ -91,7 +91,7 @@
9191
9292
async function login() {
9393
loading.value = true;
94-
const { data, error } = await api.login(email.value, loginPassword.value);
94+
const { error } = await ctx.login(api, email.value, loginPassword.value);
9595
9696
if (error) {
9797
toast.error("Invalid email or password");
@@ -101,13 +101,6 @@
101101
102102
toast.success("Logged in successfully");
103103
104-
// @ts-ignore
105-
authStore.$patch({
106-
token: data.token,
107-
expires: data.expiresAt,
108-
attachmentToken: data.attachmentToken,
109-
});
110-
111104
navigateTo("/home");
112105
loading.value = false;
113106
}

frontend/pages/profile.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup lang="ts">
22
import { Detail } from "~~/components/global/DetailsSection/types";
3-
import { useAuthStore } from "~~/stores/auth";
43
import { themes } from "~~/lib/data/themes";
54
import { currencies, Currency } from "~~/lib/data/currency";
65
@@ -79,7 +78,7 @@
7978
8079
const { setTheme } = useTheme();
8180
82-
const auth = useAuthStore();
81+
const auth = useAuthContext();
8382
8483
const details = computed(() => {
8584
return [

0 commit comments

Comments
 (0)