Skip to content

Commit

Permalink
Merge pull request #104 from manchenkoff/33-token-based-auth
Browse files Browse the repository at this point in the history
feat: added token storage support
  • Loading branch information
manchenkoff authored Jun 13, 2024
2 parents 72eb181 + 2ebcfbf commit a713d89
Show file tree
Hide file tree
Showing 18 changed files with 280 additions and 112 deletions.
29 changes: 28 additions & 1 deletion playground/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
import type { FetchContext } from 'ofetch';
import type { ConsolaInstance } from 'consola';
import type { NuxtApp } from '#app';
import { defineAppConfig } from '#imports';
import { type NuxtApp } from '#app';
import type { TokenStorage } from '../src/runtime/types/config';

const tokenStorageKey = 'sanctum.storage.token';
const localTokenStorage: TokenStorage = {
get: async () => {
if (import.meta.server) {
return undefined;
}

return window.localStorage.getItem(tokenStorageKey) ?? undefined;
},

set: async (app: NuxtApp, token?: string) => {
if (import.meta.server) {
return;
}

if (!token) {
window.localStorage.removeItem(tokenStorageKey);
return;
}

window.localStorage.setItem(tokenStorageKey, token);
},
};

export default defineAppConfig({
sanctum: {
Expand All @@ -22,5 +47,7 @@ export default defineAppConfig({
logger.debug(`custom onResponse interceptor (${ctx.request})`);
},
},

tokenStorage: localTokenStorage,
},
});
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineNuxtConfig({

sanctum: {
baseUrl: 'http://localhost:80',
mode: 'cookie',
logLevel: 5,
redirect: {
keepRequestedRoute: true,
Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SanctumModuleOptions } from './runtime/types';
import type { SanctumModuleOptions } from './runtime/types/options';

export const defaultModuleOptions: Partial<SanctumModuleOptions> = {
mode: 'cookie',
userStateKey: 'sanctum.user.identity',
redirectIfAuthenticated: false,
endpoints: {
Expand Down
46 changes: 4 additions & 42 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import {
addImportsDir,
addRouteMiddleware,
useLogger,
addTypeTemplate,
} from '@nuxt/kit';
import { defu } from 'defu';
import type {
SanctumGlobalMiddlewarePageMeta,
SanctumModuleOptions,
} from './runtime/types';
import { defaultModuleOptions } from './config';
import type { SanctumGlobalMiddlewarePageMeta } from './runtime/types/meta';
import type { SanctumModuleOptions } from './runtime/types/options';
import { registerTypeTemplates } from './templates';

type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
Expand Down Expand Up @@ -84,42 +82,6 @@ export default defineNuxtModule<ModuleOptions>({
logger.info('Sanctum module initialized w/o global middleware');
}

addTypeTemplate({
filename: 'types/sanctum.d.ts',
getContents: () => `// Generated by nuxt-auth-sanctum module
import type {
SanctumAppConfig,
SanctumGlobalMiddlewarePageMeta
} from '${resolver.resolve('./runtime/types.ts')}';
declare module 'nuxt/schema' {
interface AppConfig {
sanctum?: SanctumAppConfig;
}
interface AppConfigInput {
sanctum?: SanctumAppConfig;
}
}
declare module '@nuxt/schema' {
interface AppConfig {
sanctum?: SanctumAppConfig;
}
interface AppConfigInput {
sanctum?: SanctumAppConfig;
}
}
declare module '../../node_modules/nuxt/dist/pages/runtime/composables' {
interface PageMeta {
/**
* Sanctum global middleware page configuration.
*/
sanctum?: Partial<SanctumGlobalMiddlewarePageMeta>;
}
}
export {};`,
});
registerTypeTemplates(resolver);
},
});
2 changes: 1 addition & 1 deletion src/runtime/composables/useSanctumAppConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useAppConfig } from '#app';
import type { SanctumAppConfig } from '../types';
import type { SanctumAppConfig } from '../types/config';

export const useSanctumAppConfig = (): SanctumAppConfig => {
return (useAppConfig().sanctum ?? {}) as SanctumAppConfig;
Expand Down
16 changes: 15 additions & 1 deletion src/runtime/composables/useSanctumAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useSanctumClient } from './useSanctumClient';
import { useSanctumUser } from './useSanctumUser';
import { navigateTo, useNuxtApp, useRoute } from '#app';
import { useSanctumConfig } from './useSanctumConfig';
import { useSanctumAppConfig } from './useSanctumAppConfig';

export interface SanctumAuth<T> {
user: Ref<T | null>;
Expand All @@ -12,6 +13,10 @@ export interface SanctumAuth<T> {
refreshIdentity: () => Promise<void>;
}

export type TokenResponse = {
token: string;
};

/**
* Provides authentication methods for Laravel Sanctum
*
Expand All @@ -23,6 +28,7 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {
const user = useSanctumUser<T>();
const client = useSanctumClient();
const options = useSanctumConfig();
const appConfig = useSanctumAppConfig();

const isAuthenticated = computed(() => {
return user.value !== null;
Expand Down Expand Up @@ -57,11 +63,15 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {
);
}

await client(options.endpoints.login, {
const response = await client<TokenResponse>(options.endpoints.login, {
method: 'post',
body: credentials,
});

if (options.mode === 'token') {
await appConfig.tokenStorage!.set(nuxtApp, response.token);
}

await refreshIdentity();

if (options.redirect.keepRequestedRoute) {
Expand Down Expand Up @@ -102,6 +112,10 @@ export const useSanctumAuth = <T>(): SanctumAuth<T> => {

user.value = null;

if (options.mode === 'token') {
await appConfig.tokenStorage!.set(nuxtApp, undefined);
}

if (
options.redirect.onLogout === false ||
currentRoute.path === options.redirect.onLogout
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/composables/useSanctumConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRuntimeConfig } from '#app';
import type { SanctumModuleOptions } from '../types';
import type { SanctumModuleOptions } from '../types/options';

export const useSanctumConfig = (): SanctumModuleOptions => {
return useRuntimeConfig().public.sanctum as SanctumModuleOptions;
Expand Down
44 changes: 33 additions & 11 deletions src/runtime/httpFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import { useSanctumAppConfig } from './composables/useSanctumAppConfig';
import handleRequestCookies from './interceptors/cookie/request';
import handleResponseHeaders from './interceptors/cookie/response';
import handleRequestHeaders from './interceptors/common/request';
import type { SanctumInterceptor } from './types';
import handleRequestTokenHeader from './interceptors/token/request';
import type { SanctumAppConfig, SanctumInterceptor } from './types/config';
import type { SanctumModuleOptions } from './types/options';

export function createHttpClient(logger: ConsolaInstance): $Fetch {
const options = useSanctumConfig();
const user = useSanctumUser();
const appConfig = useSanctumAppConfig();
const nuxtApp = useNuxtApp();
function configureClientInterceptors(
requestInterceptors: SanctumInterceptor[],
responseInterceptors: SanctumInterceptor[],
options: SanctumModuleOptions,
appConfig: SanctumAppConfig
) {
if (options.mode === 'cookie') {
requestInterceptors.push(handleRequestCookies);
responseInterceptors.push(handleResponseHeaders);
}

const requestInterceptors: SanctumInterceptor[] = [
handleRequestHeaders,
handleRequestCookies,
];
const responseInterceptors: SanctumInterceptor[] = [handleResponseHeaders];
if (options.mode === 'token') {
requestInterceptors.push(handleRequestTokenHeader);
}

if (appConfig.interceptors?.onRequest) {
requestInterceptors.push(appConfig.interceptors.onRequest);
Expand All @@ -28,6 +33,23 @@ export function createHttpClient(logger: ConsolaInstance): $Fetch {
if (appConfig.interceptors?.onResponse) {
responseInterceptors.push(appConfig.interceptors.onResponse);
}
}

export function createHttpClient(logger: ConsolaInstance): $Fetch {
const options = useSanctumConfig();
const user = useSanctumUser();
const appConfig = useSanctumAppConfig();
const nuxtApp = useNuxtApp();

const requestInterceptors: SanctumInterceptor[] = [handleRequestHeaders];
const responseInterceptors: SanctumInterceptor[] = [];

configureClientInterceptors(
requestInterceptors,
responseInterceptors,
options,
appConfig
);

const httpOptions: FetchOptions = {
baseURL: options.baseUrl,
Expand Down
5 changes: 5 additions & 0 deletions src/runtime/interceptors/common/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { FetchContext } from 'ofetch';
import { type NuxtApp } from '#app';

/**
* Modify request before sending it to the Laravel API
* @param app Nuxt application instance
* @param ctx Fetch context
*/
export default async function handleRequestHeaders(
app: NuxtApp,
ctx: FetchContext
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/interceptors/cookie/request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FetchContext } from 'ofetch';
import type { ConsolaInstance } from 'consola';
import { useSanctumConfig } from '../../composables/useSanctumConfig';
import type { SanctumModuleOptions } from '../../types';
import type { SanctumModuleOptions } from '../../types/options';
import {
useCookie,
useRequestHeaders,
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/interceptors/cookie/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { FetchContext } from 'ofetch';
import type { ConsolaInstance } from 'consola';
import { navigateTo, useRequestEvent, type NuxtApp } from '#app';

/**
* Pass all cookies from the API to the client on SSR response
* @param app Nuxt application instance
* @param ctx Fetch context
* @param logger Module logger instance
*/
export default async function handleResponseHeaders(
app: NuxtApp,
ctx: FetchContext,
Expand All @@ -14,7 +20,6 @@ export default async function handleResponseHeaders(
return;
}

// pass all cookies from the API to the client on SSR response
if (import.meta.server) {
const event = useRequestEvent(app);
const serverCookieName = 'set-cookie';
Expand Down
30 changes: 30 additions & 0 deletions src/runtime/interceptors/token/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { FetchContext } from 'ofetch';
import type { ConsolaInstance } from 'consola';
import { type NuxtApp } from '#app';
import { useSanctumAppConfig } from '../../composables/useSanctumAppConfig';

/**
* Use token in authentication header for the request
* @param app Nuxt application instance
* @param ctx Fetch context
* @param logger Module logger instance
*/
export default async function handleRequestTokenHeader(
app: NuxtApp,
ctx: FetchContext,
logger: ConsolaInstance
): Promise<void> {
const appConfig = useSanctumAppConfig();

const token = await appConfig.tokenStorage!.get(app);

if (!token) {
logger.debug('Authentication token is not set in the storage');
return;
}

ctx.options.headers = {
...ctx.options.headers,
Authorization: `Bearer ${token}`,
};
}
18 changes: 17 additions & 1 deletion src/runtime/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FetchError } from 'ofetch';
import { defineNuxtPlugin, useState } from '#app';
import { defineNuxtPlugin, updateAppConfig, useState } from '#app';
import { createHttpClient } from './httpFactory';
import { useSanctumUser } from './composables/useSanctumUser';
import { useSanctumConfig } from './composables/useSanctumConfig';
import { createConsola, type ConsolaInstance } from 'consola';
import { useSanctumAppConfig } from './composables/useSanctumAppConfig';

const LOGGER_NAME = 'nuxt-auth-sanctum';

Expand Down Expand Up @@ -32,9 +33,24 @@ function handleIdentityLoadError(error: Error, logger: ConsolaInstance) {
export default defineNuxtPlugin(async () => {
const user = useSanctumUser();
const options = useSanctumConfig();
const appConfig = useSanctumAppConfig();
const logger = createSanctumLogger(options.logLevel);
const client = createHttpClient(logger);

if (options.mode === 'token' && !appConfig.tokenStorage) {
logger.debug(
'Token storage is not defined, switch to default cookie storage'
);

const defaultStorage = await import('./storages/cookieTokenStorage');

updateAppConfig({
sanctum: {
tokenStorage: defaultStorage.cookieTokenStorage,
},
});
}

const identityFetchedOnInit = useState<boolean>(
'sanctum.user.loaded',
() => false
Expand Down
26 changes: 26 additions & 0 deletions src/runtime/storages/cookieTokenStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCookie, type NuxtApp } from '#app';
import { unref } from 'vue';
import type { TokenStorage } from '../types/config';

const cookieTokenKey = 'sanctum.token.cookie';

/**
* Token storage using a secure cookie.
* Works with both CSR/SSR modes.
*/
export const cookieTokenStorage: TokenStorage = {
async get(app: NuxtApp) {
return await app.runWithContext(() => {
const cookie = useCookie(cookieTokenKey, { readonly: true });

return unref(cookie.value) ?? undefined;
});
},
async set(app: NuxtApp, token?: string) {
await app.runWithContext(() => {
const cookie = useCookie(cookieTokenKey, { secure: true });

cookie.value = token;
});
},
};
Loading

0 comments on commit a713d89

Please sign in to comment.