Skip to content

Commit

Permalink
Merge pull request #3 from becem-gharbi/acess-token-cookie-based
Browse files Browse the repository at this point in the history
Acess token cookie based
  • Loading branch information
becem-gharbi authored Jan 21, 2023
2 parents 20bdb71 + 0245f59 commit 13779f7
Show file tree
Hide file tree
Showing 19 changed files with 250 additions and 207 deletions.
2 changes: 0 additions & 2 deletions playground/pages/auth/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ const { login, requestPasswordReset, loginWithProvider } = useAuth()
async function handleLogin() {
const { data, error } = await login({ email: "[email protected]", password: "123456" })
console.log(data.value?.accessToken)
console.error(error.value?.data?.message)
}
async function handleRequestPasswordReset() {
Expand Down
12 changes: 3 additions & 9 deletions playground/pages/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@
<h1>Home</h1>
<button @click="fetchUser">Fetch user</button>
<button @click="handleLogout">Logout</button>
<p>{{ user }}</p>
</div>
</template>

<script setup lang="ts">
import { User } from '.prisma/client';
definePageMeta({ middleware: "auth" })
const { logout } = useAuth()
async function fetchUser() {
const data = await useAuthFetch<User>("/api/auth/me")
console.log(data)
}
const { logout, fetchUser, useUser } = useAuth()
const user = useUser()
async function handleLogout() {
await logout()
Expand Down
6 changes: 6 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default defineNuxtModule<ModuleOptions>({
refreshTokenSecret: "efg",
accessTokenExpiresIn: "7s",
refreshTokenMaxAge: 3600,
accessTokenMaxAge: 10,

smtp: {
host: "",
Expand All @@ -36,6 +37,9 @@ export default defineNuxtModule<ModuleOptions>({
baseUrl: "http://localhost:3000",
enableGlobalAuthMiddleware: false,
refreshTokenCookieName: "auth_refresh_token",

accessTokenCookieName: "auth_access_token",

redirect: {
login: "/auth/login",
logout: "/auth/login",
Expand Down Expand Up @@ -147,6 +151,7 @@ export default defineNuxtModule<ModuleOptions>({
refreshTokenSecret: options.refreshTokenSecret,
accessTokenExpiresIn: options.accessTokenExpiresIn,
refreshTokenMaxAge: options.refreshTokenMaxAge,
accessTokenMaxAge: options.accessTokenMaxAge,
accessTokenClaims: options.accessTokenClaims,

emailTemplates: options.emailTemplates,
Expand All @@ -160,6 +165,7 @@ export default defineNuxtModule<ModuleOptions>({
baseUrl: options.baseUrl,
enableGlobalAuthMiddleware: options.enableGlobalAuthMiddleware,
refreshTokenCookieName: options.refreshTokenCookieName,
accessTokenCookieName: options.accessTokenCookieName,
redirect: {
login: options.redirect.login,
logout: options.redirect.logout,
Expand Down
169 changes: 82 additions & 87 deletions src/runtime/composables/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import jwt_decode from "jwt-decode";
import type { Ref } from "vue";
import { Ref } from "vue";
import { appendHeader } from "h3";
import type { User, Provider } from "../types";
import type { AsyncData } from "#app";
import type { FetchError } from "ofetch";
import useAuthFetch from "./useAuthFetch";
import jwtDecode from "jwt-decode";

import {
useRuntimeConfig,
Expand All @@ -27,47 +28,57 @@ type FetchReturn<T> = Promise<AsyncData<UseFetchDataT<T>, UseFetchErrorT>>;

export default function () {
const publicConfig = useRuntimeConfig().public.auth;
const useInitialized: () => Ref<boolean> = () =>
useState("auth_initialized", () => false);
const useUser: () => Ref<User | null> = () =>
useState<User | null>("auth_user", () => null);
const useAccessToken: () => Ref<string | null> = () =>
useState<string | null>("auth_access_token", () => null);

const useUser: () => Ref<User | null | undefined> = () =>
useState<User | null | undefined>("auth_user", () => null);

const useAccessToken: () => Ref<string | undefined | null> = () =>
useState<string | undefined | null>("auth_access_token", () => null);

const useAccessTokenCookie = () =>
useCookie(publicConfig.accessTokenCookieName);

const useRefreshTokenCookie = () =>
useCookie(publicConfig.refreshTokenCookieName);

const event = useRequestEvent();
const route = useRoute();

function isAccessTokenExpired() {
const accessToken = useAccessToken();

if (accessToken.value) {
const decoded = jwt_decode(accessToken.value) as { exp: number };
const decoded = jwtDecode(accessToken.value) as { exp: number };
const expires = decoded.exp * 1000;
return expires < Date.now();
}

return true;
}

async function login(input: {
email: string;
password: string;
}): FetchReturn<{ accessToken: string }> {
const accessToken = useAccessToken();

return useFetch<UseFetchDataT<{ accessToken: string }>, UseFetchErrorT>(
"/api/auth/login",
{
method: "POST",
credentials: "include",
body: {
email: input.email,
password: input.password,
},
}
).then(async (res) => {
if (res.data.value) {
accessToken.value = res.data.value.accessToken;
await fetchUser();
}): FetchReturn<{ accessToken: string; user: User }> {
return useFetch<
UseFetchDataT<{ accessToken: string; user: User }>,
UseFetchErrorT
>("/api/auth/login", {
method: "POST",
body: {
email: input.email,
password: input.password,
},
}).then(async (res) => {
const accessToken = useAccessToken();
const user = useUser();

accessToken.value = res.data.value?.accessToken;
user.value = res.data.value?.user;

if (accessToken.value) {
await navigateTo(publicConfig.redirect.home);
}

return res;
});
}
Expand All @@ -80,92 +91,76 @@ export default function () {

async function prefetch(): Promise<void> {
const accessToken = useAccessToken();
if (accessToken.value) {
if (isAccessTokenExpired()) {
await refresh();
if (!accessToken.value) {
await logout();
throw new Error("Unauthorized");
}
}

await refresh();

if (!accessToken.value) {
await logout();
throw new Error("unauthorized");
}
}

async function refresh(): Promise<void> {
let cookie: string | undefined;
const accessToken = useAccessToken();

try {
const accessToken = useAccessToken();
const user = useUser();

if (process.server) {
const headers = useRequestHeaders(["Cookie"]);
cookie = headers.cookie;
accessToken.value = useAccessTokenCookie().value;
} else {
accessToken.value = isAccessTokenExpired() ? null : accessToken.value;
}

if (!cookie || !cookie.includes(publicConfig.refreshTokenCookieName)) {
accessToken.value = null;
return;
if (accessToken.value) {
if (!user.value) {
user.value = await $fetch<User>("/api/auth/me", {
headers: {
Authorization: "Bearer " + accessToken.value,
},
});
}
return;
}

const res = await $fetch.raw<{ accessToken: string }>(
if (process.server && !useRefreshTokenCookie().value) {
return;
}

const cookie = useRequestHeaders(["cookie"]).cookie || "";

const res = await $fetch.raw<{ accessToken: string; user: User }>(
"/api/auth/refresh",
{
method: "POST",
credentials: "include",
headers: cookie ? { cookie } : {},
body: {
accessToken: accessToken.value,
},
headers: process.server ? { cookie } : {},
}
);

if (process.server) {
const cookie = res.headers.get("set-cookie") || "";
appendHeader(event, "set-cookie", cookie);
const cookies = (res.headers.get("set-cookie") || "").split(",");
for (const cookie of cookies) {
appendHeader(event, "set-cookie", cookie);
}
}

accessToken.value = res._data?.accessToken || null;
} catch (error) {
accessToken.value = null;
}
accessToken.value = res._data?.accessToken;
user.value = res._data?.user;
} catch (e) {}
}

async function fetchUser(): Promise<void> {
const accessToken = useAccessToken();
const user = useUser();

if (!accessToken.value) {
user.value = null;
return;
}

await prefetch();

try {
user.value = await $fetch<User>("/api/auth/me", {
headers: {
Authorization: "Bearer " + accessToken.value,
},
});
} catch (error) {
user.value = null;
}
user.value = await useAuthFetch<User>("/api/auth/me");
}

async function logout(): Promise<void> {
const accessToken = useAccessToken();
const user = useUser();

if (accessToken.value) {
await $fetch("/api/auth/logout", {
method: "POST",
credentials: "include",
});

await $fetch("/api/auth/logout", {
method: "POST",
}).finally(() => {
const accessToken = useAccessToken();
accessToken.value = null;
}

user.value = null;
await navigateTo(publicConfig.redirect.logout);
return navigateTo(publicConfig.redirect.logout);
});
}

async function register(input: {
Expand All @@ -192,6 +187,7 @@ export default function () {
}

async function resetPassword(password: string): FetchReturn<void> {
const route = useRoute();
return useFetch<UseFetchDataT<void>, UseFetchErrorT>(
"/api/auth/password/reset",
{
Expand All @@ -217,7 +213,6 @@ export default function () {
}

return {
useInitialized,
useUser,
useAccessToken,
login,
Expand Down
18 changes: 8 additions & 10 deletions src/runtime/composables/useAuthFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,16 @@ export default async function <DataT>(
): Promise<DataT> {
const { useAccessToken, prefetch } = useAuth();

const accessToken = useAccessToken();

await prefetch();

if (accessToken.value) {
fetchOptions.headers = defu(
{
Authorization: "Bearer " + accessToken.value,
},
fetchOptions.headers
);
}
const accessToken = useAccessToken();

fetchOptions.headers = defu(
{
Authorization: "Bearer " + accessToken.value,
},
fetchOptions.headers
);

return $fetch<DataT>(path, fetchOptions);
}
16 changes: 8 additions & 8 deletions src/runtime/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import { defineNuxtRouteMiddleware, useRuntimeConfig, navigateTo } from "#app";
import useAuth from "../composables/useAuth";

export default defineNuxtRouteMiddleware((to) => {
const config = useRuntimeConfig();
const publicConfig = useRuntimeConfig().public.auth;

if (
to.path === config.public.auth.redirect.login ||
to.path === config.public.auth.redirect.callback
to.path === publicConfig.redirect.login ||
to.path === publicConfig.redirect.callback
) {
return;
}

if (config.public.auth.enableGlobalAuthMiddleware === true) {
if (publicConfig.enableGlobalAuthMiddleware === true) {
if (to.meta.auth === false) {
return;
}
}

const { useUser } = useAuth();
const user = useUser();
const { useAccessToken } = useAuth();

if (!user.value) {
return navigateTo(config.public.auth.redirect.login);
if (!useAccessToken().value) {
console.log("from auth middle redirect ro login");
return navigateTo(publicConfig.redirect.login);
}
});
Loading

0 comments on commit 13779f7

Please sign in to comment.