diff --git a/package.json b/package.json index 69c6c65..063557c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nuxt-alt/auth", - "version": "3.1.4", + "version": "3.1.7", "description": "An alternative module to @nuxtjs/auth", "homepage": "https://github.com/nuxt-alt/auth", "author": "Denoder", @@ -37,24 +37,24 @@ }, "dependencies": { "@nuxt-alt/http": "latest", - "@nuxt/kit": "^3.9.1", + "@nuxt/kit": "^3.12.2", "@refactorjs/serialize": "latest", - "cookie-es": "^1.0.0", + "cookie-es": "^1.1.0", "defu": "^6.1.3", "jwt-decode": "^4.0.0", "ohash": "^1.1.3", - "pathe": "^1.1.1", + "pathe": "^1.1.2", "pinia": "^2.1.7", "requrl": "^3.0.2" }, "devDependencies": { - "@nuxt-alt/proxy": "^2.4.8", - "@nuxt/schema": "^3.9.1", + "@nuxt-alt/proxy": "^2.5.8", + "@nuxt/schema": "^3.12.2", "@nuxtjs/i18n": "next", "@types/node": "^20", - "jiti": "^1.21.0", - "nuxt": "^3.9.1", - "typescript": "^5.3.3", + "jiti": "^1.21.6", + "nuxt": "^3.9.3", + "typescript": "5.3.3", "unbuild": "^2.0.0" }, "repository": { diff --git a/src/plugin.ts b/src/plugin.ts index 5a1ba3c..82a0fef 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,10 +1,10 @@ -import type { ModuleOptions, Strategy, ImportOptions } from './types'; +import type { ModuleOptions, StrategyOptions, ImportOptions } from './types'; import { serialize } from '@refactorjs/serialize'; export const getAuthPlugin = (options: { options: ModuleOptions schemeImports: ImportOptions[] - strategies: Strategy[] + strategies: StrategyOptions[] strategyScheme: Record }): string => { return `import { Auth, ExpiredAuthSessionError } from '#auth/runtime' diff --git a/src/resolve.ts b/src/resolve.ts index 77332bc..576fb88 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -1,23 +1,24 @@ -import type { Strategy, ModuleOptions, ProviderNames, SchemeNames, ImportOptions } from './types'; +import type { StrategyOptions, ModuleOptions, SchemeNames, ImportOptions } from './types'; import type { Nuxt } from '@nuxt/schema'; -import { OAUTH2DEFAULTS, LOCALDEFAULTS, ProviderAliases, BuiltinSchemes } from './runtime/inc/default-properties'; -import { addAuthorize, addLocalAuthorize, assignAbsoluteEndpoints, assignDefaults } from './utils/provider'; +import { OAUTH2DEFAULTS, LOCALDEFAULTS, ProviderAliases, BuiltinSchemes, LocalSchemes, OAuth2Schemes } from './runtime/inc/default-properties'; +import { addAuthorize, addLocalAuthorize, assignAbsoluteEndpoints, assignDefaults, } from './utils/provider'; +import { hasOwn } from './utils'; import * as AUTH_PROVIDERS from './runtime/providers'; import { resolvePath } from '@nuxt/kit'; import { existsSync } from 'fs'; import { hash } from 'ohash'; export async function resolveStrategies(nuxt: Nuxt, options: ModuleOptions) { - const strategies: Strategy[] = []; + const strategies: StrategyOptions[] = []; const strategyScheme = {} as Record; for (const name of Object.keys(options.strategies!)) { - if (!options.strategies![name] || (options.strategies as Strategy)[name].enabled === false) { + if (!options.strategies?.[name] || options.strategies?.[name].enabled === false) { continue; } // Clone strategy - const strategy = Object.assign({}, options.strategies![name]) as Strategy; + const strategy = Object.assign({}, options.strategies![name]); // Default name if (!strategy.name) { @@ -26,14 +27,18 @@ export async function resolveStrategies(nuxt: Nuxt, options: ModuleOptions) { // Default provider (same as name) if (!strategy.provider) { - strategy.provider = strategy.name as ProviderNames; + strategy.provider = strategy.name; } // Determine if SSR is enabled - strategy.ssr = nuxt.options.ssr + if (hasOwn(strategy, 'ssr')) { + strategy.ssr = strategy.ssr; + } else { + strategy.ssr = nuxt.options.ssr; + } // Try to resolve provider - const provider = await resolveProvider(strategy.provider, nuxt, strategy); + const provider = await resolveProvider(strategy.provider as string, nuxt, strategy); delete strategy.provider; @@ -50,7 +55,7 @@ export async function resolveStrategies(nuxt: Nuxt, options: ModuleOptions) { // Resolve and keep scheme needed for strategy const schemeImport = await resolveScheme(strategy.scheme); delete strategy.scheme; - strategyScheme[strategy.name] = schemeImport as ImportOptions; + strategyScheme[strategy.name] = schemeImport!; // Add strategy to array strategies.push(strategy); @@ -90,7 +95,7 @@ export async function resolveScheme(scheme: string) { } } -export async function resolveProvider(provider: string | ((...args: any[]) => any), nuxt: Nuxt, strategy: Strategy) { +export async function resolveProvider(provider: string | ((nuxt: Nuxt, strategy: StrategyOptions, ...args: any[]) => void), nuxt: Nuxt, strategy: StrategyOptions) { provider = (ProviderAliases[provider as keyof typeof ProviderAliases] || provider); if (AUTH_PROVIDERS[provider as keyof typeof AUTH_PROVIDERS]) { @@ -104,20 +109,20 @@ export async function resolveProvider(provider: string | ((...args: any[]) => an // return an empty function as it doesn't use a provider if (typeof provider === 'string') { - return (nuxt: Nuxt, strategy: Strategy) => { - if (['oauth2', 'openIDConnect', 'auth0'].includes(strategy.scheme!) && strategy.ssr) { - assignDefaults(strategy as any, OAUTH2DEFAULTS) - addAuthorize(nuxt, strategy as any, true) + return (nuxt: Nuxt, strategy: StrategyOptions) => { + if (OAuth2Schemes.includes(strategy.scheme!) && strategy.ssr) { + assignDefaults(strategy, OAUTH2DEFAULTS as typeof strategy) + addAuthorize(nuxt, strategy, true) } - if (['refresh', 'local', 'cookie'].includes(strategy.scheme!) && strategy.ssr) { - assignDefaults(strategy as any, LOCALDEFAULTS) + if (LocalSchemes.includes(strategy.scheme!) && strategy.ssr) { + assignDefaults(strategy, LOCALDEFAULTS as typeof strategy) if (strategy.url) { - assignAbsoluteEndpoints(strategy as any); + assignAbsoluteEndpoints(strategy); } - addLocalAuthorize(nuxt, strategy as any) + addLocalAuthorize(nuxt, strategy) } } } diff --git a/src/runtime/core/storage.ts b/src/runtime/core/storage.ts index 97fd9d5..4c5a66e 100644 --- a/src/runtime/core/storage.ts +++ b/src/runtime/core/storage.ts @@ -1,11 +1,17 @@ import type { ModuleOptions, AuthStore, AuthState, StoreMethod, StoreIncludeOptions } from '../../types'; import type { NuxtApp } from '#app'; -import type { Pinia, StoreDefinition } from 'pinia'; +import { type Pinia, type StoreDefinition, defineStore } from 'pinia'; import { isUnset, isSet, decodeValue, encodeValue, setH3Cookie } from '../../utils'; import { parse, serialize, type CookieSerializeOptions } from 'cookie-es'; -import { useState } from '#imports'; import { watch, type Ref } from 'vue'; +import { useState } from '#imports'; +/** + * @class Storage + * @classdesc Storage class for stores and cookies + * @param { NuxtApp } ctx - Nuxt app context + * @param { ModuleOptions } options - Module options + */ export class Storage { ctx: NuxtApp; options: ModuleOptions; @@ -13,7 +19,8 @@ export class Storage { #initPiniaStore!: AuthStore; #initStore!: Ref; state: AuthState; - memory!: Ref; + #internal!: Ref; + memory!: AuthState; #piniaEnabled: boolean = false; constructor(ctx: NuxtApp, options: ModuleOptions) { @@ -106,22 +113,22 @@ export class Storage { this.#piniaEnabled = this.options.stores.pinia!?.enabled! && !!pinia; if (this.#piniaEnabled) { - const { defineStore } = await import('pinia') - this.#PiniaStore = defineStore(this.options.stores.pinia?.namespace!, { + this.#PiniaStore = defineStore(this.options.stores.pinia?.namespace as string, { state: (): AuthState => ({ ...this.options.initialState }) }); this.#initPiniaStore = this.#PiniaStore(pinia) this.state = this.#initPiniaStore; } else { - this.#initStore = useState(this.options.stores.state?.namespace, () => ({ + this.#initStore = useState(this.options.stores.state?.namespace as string, () => ({ ...this.options.initialState })) this.state = this.#initStore.value } - this.memory = useState('auth-internal', () => ({})) + this.#internal = useState('auth-internal', () => ({})) + this.memory = this.#internal.value } get pinia() { @@ -134,7 +141,7 @@ export class Storage { setState(key: string, value: any) { if (key.startsWith('_')) { - this.memory.value[key] = value; + this.memory[key] = value; } else if (this.#piniaEnabled) { this.#initPiniaStore.$patch({ [key]: value }); @@ -150,7 +157,7 @@ export class Storage { if (!key.startsWith('_')) { return this.state[key]; } else { - return this.memory.value[key]; + return this.memory[key]; } } @@ -214,7 +221,7 @@ export class Storage { isLocalStorageEnabled(): boolean { const isNotServer = !process.server; const isConfigEnabled = this.options.stores.local?.enabled; - const localTest = "test"; + const localTest = 'test'; if (isNotServer && isConfigEnabled) { try { @@ -278,7 +285,7 @@ export class Storage { const isNotServer = !process.server; // @ts-ignore const isConfigEnabled = this.options.stores!.session?.enabled; - const testKey = "test"; + const testKey = 'test'; if (isNotServer && isConfigEnabled) { try { diff --git a/src/runtime/inc/default-properties.ts b/src/runtime/inc/default-properties.ts index 32ffdd9..6a910e9 100644 --- a/src/runtime/inc/default-properties.ts +++ b/src/runtime/inc/default-properties.ts @@ -116,9 +116,22 @@ export const ProviderAliases = { export const BuiltinSchemes = { local: 'LocalScheme', cookie: 'CookieScheme', - oauth2: 'Oauth2Scheme', - openIDConnect: 'OpenIDConnectScheme', refresh: 'RefreshScheme', laravelJWT: 'LaravelJWTScheme', + oauth2: 'Oauth2Scheme', + openIDConnect: 'OpenIDConnectScheme', auth0: 'Auth0Scheme', -}; \ No newline at end of file +}; + +export const LocalSchemes = [ + 'local', + 'cookie', + 'refresh', + 'laravelJWT', +] + +export const OAuth2Schemes = [ + 'oauth', + 'openIDConnect', + 'auth0', +] \ No newline at end of file diff --git a/src/runtime/inc/id-token.ts b/src/runtime/inc/id-token.ts index eb6b63e..4ded8ae 100644 --- a/src/runtime/inc/id-token.ts +++ b/src/runtime/inc/id-token.ts @@ -36,14 +36,13 @@ export class IdToken { } reset() { - this.scheme.requestHandler!.clearHeader(); this.#resetSSRToken(); this.#setToken(undefined); this.#setExpiration(undefined); } status(): TokenStatus { - return new TokenStatus(this.get(), this.#getExpiration()); + return new TokenStatus(this.get(), this.#getExpiration(), this.scheme.options.idToken?.httpOnly); } #resetSSRToken(): void { diff --git a/src/runtime/inc/refresh-token.ts b/src/runtime/inc/refresh-token.ts index 7f96895..310e958 100644 --- a/src/runtime/inc/refresh-token.ts +++ b/src/runtime/inc/refresh-token.ts @@ -36,13 +36,13 @@ export class RefreshToken { } reset(): void { - this.#resetSSRToken() + this.#resetSSRToken(); this.#setToken(undefined); this.#setExpiration(undefined); } status(): TokenStatus { - return new TokenStatus(this.get(), this.#getExpiration(), this.scheme.options.refreshToken.httpOnly); + return new TokenStatus(this.get(), this.#getExpiration(), this.scheme.options.refreshToken?.httpOnly); } #resetSSRToken(): void { diff --git a/src/runtime/inc/request-handler.ts b/src/runtime/inc/request-handler.ts index 0425a2f..8b0e533 100644 --- a/src/runtime/inc/request-handler.ts +++ b/src/runtime/inc/request-handler.ts @@ -17,7 +17,7 @@ export class RequestHandler { this.auth = auth; this.requestInterceptor = null; this.responseErrorInterceptor = null; - this.currentToken = this.auth.$storage.memory.value?.[this.scheme.options.token!.prefix + this.scheme.options.name] as string + this.currentToken = this.auth.$storage?.memory?.[this.scheme.options.token!?.prefix + this.scheme.options.name] as string } setHeader(token: string): void { diff --git a/src/runtime/schemes/auth0.ts b/src/runtime/schemes/auth0.ts index 2be0cb6..9655240 100644 --- a/src/runtime/schemes/auth0.ts +++ b/src/runtime/schemes/auth0.ts @@ -2,7 +2,7 @@ import { withQuery } from 'ufo'; import { Oauth2Scheme } from '../schemes/oauth2'; export class Auth0Scheme extends Oauth2Scheme { - logout(): void { + override logout(): void { this.$auth.reset(); const opts = { diff --git a/src/runtime/schemes/cookie.ts b/src/runtime/schemes/cookie.ts index f1e7c4a..17b1608 100644 --- a/src/runtime/schemes/cookie.ts +++ b/src/runtime/schemes/cookie.ts @@ -45,7 +45,7 @@ export class CookieScheme extends LocalSch super($auth, options as OptionsT, DEFAULTS as OptionsT); } - async mounted(): Promise | void> { + override async mounted(): Promise | void> { if (process.server) { this.$auth.ctx.$http.setHeader('referer', this.$auth.ctx.ssrContext!.event.node.req.headers.host!); } @@ -59,7 +59,7 @@ export class CookieScheme extends LocalSch return this.$auth.fetchUserOnce(); } - check(): SchemeCheck { + override check(): SchemeCheck { const response = { valid: false }; if (!super.check().valid && this.options.token?.type) { @@ -82,7 +82,7 @@ export class CookieScheme extends LocalSch return response; } - async login(endpoint: HTTPRequest): Promise | void> { + override async login(endpoint: HTTPRequest): Promise | void> { // Ditch any leftover local tokens before attempting to log in this.$auth.reset(); @@ -119,7 +119,7 @@ export class CookieScheme extends LocalSch return response; } - async fetchUser(endpoint?: HTTPRequest): Promise | void> { + override async fetchUser(endpoint?: HTTPRequest): Promise | void> { if (!this.check().valid) { return Promise.resolve(); } @@ -156,7 +156,7 @@ export class CookieScheme extends LocalSch }); } - reset(): void { + override reset(): void { if (this.options.cookie.name) { this.$auth.$storage.setCookie(this.options.cookie.name, null); } diff --git a/src/runtime/schemes/laravel-jwt.ts b/src/runtime/schemes/laravel-jwt.ts index 3e16582..ff0c1da 100644 --- a/src/runtime/schemes/laravel-jwt.ts +++ b/src/runtime/schemes/laravel-jwt.ts @@ -2,7 +2,7 @@ import type { HTTPResponse } from '../../types'; import { RefreshScheme } from './refresh'; export class LaravelJWTScheme extends RefreshScheme { - protected updateTokens(response: HTTPResponse): void { + protected override updateTokens(response: HTTPResponse): void { super.updateTokens(response); } } diff --git a/src/runtime/schemes/local.ts b/src/runtime/schemes/local.ts index 9dc442f..a5e8ff5 100644 --- a/src/runtime/schemes/local.ts +++ b/src/runtime/schemes/local.ts @@ -128,6 +128,7 @@ export class LocalScheme): void { + protected override updateTokens(response: HTTPResponse): void { super.updateTokens(response); const idToken = getProp(response._data, this.options.idToken.property) as string; @@ -55,7 +54,7 @@ export class OpenIDConnectScheme | void> { + override mounted(): Promise | void> { return super.mounted({ tokenCallback: () => { if (this.options.autoLogout) { @@ -162,7 +162,7 @@ export class RefreshScheme | void> { + override setUserToken(token: string | boolean, refreshToken?: string | boolean): Promise | void> { this.token.set(token); if (refreshToken) { @@ -173,7 +173,7 @@ export class RefreshScheme): void { + protected override updateTokens(response: HTTPResponse): void { let tokenExpiresIn: number | boolean = false const token = this.options.token?.required ? this.extractToken(response) : true; const refreshToken = this.options.refreshToken.required ? this.extractRefreshToken(response) : true; @@ -200,7 +200,7 @@ export class RefreshScheme { diff --git a/src/types/options.d.ts b/src/types/options.d.ts index 30923bd..32a09e0 100644 --- a/src/types/options.d.ts +++ b/src/types/options.d.ts @@ -1,4 +1,4 @@ -import type { Strategy } from './strategy'; +import type { Strategy, StrategyOptions } from './strategy'; import type { NuxtPlugin } from '@nuxt/schema'; import type { AuthState, RefreshableScheme, TokenableScheme } from './index'; import type { CookieSerializeOptions } from 'cookie-es'; @@ -24,7 +24,7 @@ export interface ModuleOptions { /** * Authentication strategies used by the module. */ - strategies?: Record; + strategies?: Record; /** * Whether exceptions should be ignored or not. diff --git a/src/types/provider.d.ts b/src/types/provider.d.ts index 01f4a22..98f1b65 100644 --- a/src/types/provider.d.ts +++ b/src/types/provider.d.ts @@ -1,7 +1,9 @@ -import type { SchemeOptions } from './scheme'; +import type { SchemeOptions, SchemeNames } from './scheme'; +import type { StrategyOptions } from './strategy'; import type { PartialExcept } from './utils'; +import type { Nuxt } from '@nuxt/schema'; -export type ProviderNames = 'laravel/sanctum' | 'laravel/jwt' | 'laravel/passport' | 'google' | 'github' | 'facebook' | 'discord' | 'auth0' | N | ((...args: any[]) => any) +export type ProviderNames = 'laravel/sanctum' | 'laravel/jwt' | 'laravel/passport' | 'google' | 'github' | 'facebook' | 'discord' | 'auth0' | N | ((nuxt: Nuxt, strategy: StrategyOptions, ...args: any[]) => void); export interface ImportOptions { name: string; @@ -10,7 +12,7 @@ export interface ImportOptions { } export interface ProviderOptions { - scheme: string; + scheme?: SchemeNames; clientSecret: string | number; } diff --git a/src/types/strategy.d.ts b/src/types/strategy.d.ts index b21dd1f..8c9b1ec 100644 --- a/src/types/strategy.d.ts +++ b/src/types/strategy.d.ts @@ -1,17 +1,16 @@ import type { SchemePartialOptions, RefreshableSchemeOptions, SchemeOptions, SchemeNames } from './scheme'; import type { CookieSchemeOptions, Oauth2SchemeOptions, OpenIDConnectSchemeOptions } from '../runtime/schemes'; import type { ProviderPartialOptions, ProviderOptions, ProviderNames } from './provider'; +import type { RecursivePartial } from './utils'; export type Strategy = S & Strategies; // @ts-ignore: endpoints dont match export interface AuthSchemeOptions extends RefreshableSchemeOptions, Oauth2SchemeOptions, CookieSchemeOptions, OpenIDConnectSchemeOptions {} -export interface Strategies extends SchemePartialOptions { - provider?: ProviderNames | ((...args: any[]) => any); - scheme?: SchemeNames; +export interface Strategies { + provider?: ProviderNames; enabled?: boolean; - [key: string]: any; } -export type StrategyOptions = ProviderPartialOptions; +export type StrategyOptions = RecursivePartial> = ProviderPartialOptions; diff --git a/src/utils/provider.ts b/src/utils/provider.ts index 0fc897e..441a47f 100644 --- a/src/utils/provider.ts +++ b/src/utils/provider.ts @@ -50,6 +50,7 @@ export function addAuthorize>(nuxt: Nuxt, strategy: SOptions): void { const tokenEndpoint = strategy.endpoints?.login?.url; const refreshEndpoint = strategy.endpoints?.refresh?.url; + // Endpoint const endpoint = `/_auth/local/${strategy.name}/authorize`; strategy.endpoints!.login!.url = endpoint; @@ -203,12 +204,7 @@ export default defineEventHandler(async (event) => { delete body.client_secret } - if (options.useForms) { - body = new URLSearchParams(body).toString() - headers['Content-Type'] = 'application/x-www-form-urlencoded' - } - - const response = await event.$http.post(options.tokenEndpoint, { + const response = await $http.post(options.tokenEndpoint, { body, headers }) @@ -284,11 +280,31 @@ export default defineEventHandler(async (event) => { delete body[refreshTokenDataName] } - const authorizationURL = body[refreshTokenDataName] ? options.refreshEndpoint : options.tokenEndpoint + const headers = { + 'Content-Type': 'application/json' + } - const response = await event.$http.post(authorizationURL, { - body: new URLSearchParams(body) - }) + let response + + if (body[refreshTokenDataName]) { + response = await $http.post(options.refreshEndpoint, { + body, + headers: { + ...headers, + // @ts-ignore: headers might not be set + ...options.strategy?.endpoints?.refresh?.headers + } + }) + } else { + response = await $http.post(options.tokenEndpoint, { + body, + headers: { + ...headers, + // @ts-ignore: headers might not be set + ...options.strategy?.endpoints?.login?.headers + } + }) + } let cookies = event.node.res.getHeader('Set-Cookie') as string[] || []; @@ -350,7 +366,7 @@ export default defineEventHandler(async (event) => { }) } - const response = await event.$http.post(options.tokenEndpoint, { + const response = await $http.post(options.tokenEndpoint, { baseURL: requrl(event.node.req), body: { client_id: options.clientId,