Skip to content

Commit

Permalink
Merge branch 'main' into feature/authjs-migration-new
Browse files Browse the repository at this point in the history
  • Loading branch information
phoenix-ru authored Dec 12, 2024
2 parents a6d41a3 + b1e4aba commit 01fecc5
Show file tree
Hide file tree
Showing 32 changed files with 801 additions and 219 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [main]

env:
NODE_VER: 22.5
NODE_VER: 22.11
CI: true

jobs:
Expand Down Expand Up @@ -37,6 +37,9 @@ jobs:
# Check linting and typing
- run: pnpm lint
- run: pnpm typecheck

# Run unit tests
- run: pnpm test:unit

# Check building
- run: pnpm build
Expand Down Expand Up @@ -131,5 +134,5 @@ jobs:
# start prod-app and curl from it
- run: "timeout 60 pnpm start & (sleep 45 && curl --fail localhost:$PORT)"
env:
AUTH_ORIGIN: "http://localhost:3001"
AUTH_ORIGIN: "http://localhost:3001/api/auth"
PORT: 3001
2 changes: 1 addition & 1 deletion .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

env:
NODE_VER: 22.5
NODE_VER: 22.11
CI: true

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pkg.pr.new.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
pull_request:

env:
NODE_VER: 22.5
NODE_VER: 22.11

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/application-side/protecting-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ If the global middleware is disabled, you can manually add the middleware to ind
```vue
<script lang="ts" setup>
definePageMeta({
middleware: 'auth'
middleware: 'sidebase-auth'
})
</script>
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/local/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export default defineNuxtConfig({
refreshOnlyToken: true,
token: {
signInResponseRefreshTokenPointer: '/refresh-token',
refreshResponseTokenPointer: '',
refreshRequestTokenPointer: '/refresh-token',
cookieName: 'auth.token',
maxAgeInSeconds: 1800,
Expand Down Expand Up @@ -291,6 +292,19 @@ E.g., setting this to `/token/refreshToken` and returning an object like `{ toke

This follows the JSON Pointer standard, see its RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901

#### `refreshResponseTokenPointer`

- **Type:** `string`
- **Default:** `''`

How to extract the authentication-token from the refresh response.

E.g., setting this to `/token/bearer` and returning an object like `{ token: { bearer: 'THE_AUTH_TOKEN' }, timestamp: '2023' }` from the `refresh` endpoint will result in `nuxt-auth` extracting and storing `THE_AUTH_TOKEN`.

If not set, `token.signInResponseTokenPointer` will be used instead.

This follows the JSON Pointer standard, see its RFC6901 here: https://www.rfc-editor.org/rfc/rfc6901

#### `refreshRequestTokenPointer`

- **Type:** `string`
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"dev:prepare": "nuxt-module-build build --stub",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"test:unit": "vitest"
},
"dependencies": {
"@nuxt/kit": "^3.12.4",
Expand Down Expand Up @@ -62,6 +63,7 @@
"ts-essentials": "^9.4.2",
"typescript": "^5.5.4",
"vitepress": "^1.3.1",
"vitest": "^1.6.0",
"vue-tsc": "^2.0.29"
},
"packageManager": "[email protected]+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
Expand Down
2 changes: 1 addition & 1 deletion playground-authjs/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default defineNuxtConfig({
globalAppMiddleware: {
isEnabled: true
},
baseURL: `http://localhost:${process.env.PORT || 3000}`
baseURL: `http://localhost:${process.env.PORT || 3000}/api/auth`
},
routeRules: {
'/with-caching': {
Expand Down
1 change: 1 addition & 0 deletions playground-local/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default defineNuxtConfig({
endpoint: { path: '/refresh', method: 'post' },
token: {
signInResponseRefreshTokenPointer: '/token/refreshToken',
refreshResponseTokenPointer: '',
refreshRequestTokenPointer: '/refreshToken'
},
}
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 24 additions & 24 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ import {
useLogger
} from '@nuxt/kit'
import { defu } from 'defu'
import { joinURL } from 'ufo'
import { genInterface } from 'knitwork'
import type { DeepRequired } from 'ts-essentials'
import type { NuxtModule } from 'nuxt/schema'
import { getOriginAndPathnameFromURL, isProduction } from './runtime/helpers'
import { isProduction } from './runtime/helpers'
import type {
AuthProviders,
ModuleOptions,
Expand All @@ -26,6 +25,8 @@ import type {

const topLevelDefaults = {
isEnabled: true,
baseURL: '/api/auth',
disableInternalRouting: false as boolean,
disableServerSideAuth: false,
originEnvKey: 'AUTH_ORIGIN',
sessionRefresh: {
Expand Down Expand Up @@ -77,6 +78,7 @@ const defaultsByBackend: {
refreshOnlyToken: true,
token: {
signInResponseRefreshTokenPointer: '/refreshToken',
refreshResponseTokenPointer: '',
refreshRequestTokenPointer: '/refreshToken',
cookieName: 'auth.refresh-token',
maxAgeInSeconds: 60 * 60 * 24 * 7, // 7 days
Expand Down Expand Up @@ -107,26 +109,16 @@ export default defineNuxtModule<ModuleOptions>({
const logger = useLogger(PACKAGE_NAME)

// 0. Assemble all options
const { origin, pathname = '/api/auth' } = getOriginAndPathnameFromURL(
userOptions.baseURL ?? ''
)

const selectedProvider = userOptions.provider?.type ?? 'authjs'

const options = {
...defu(userOptions, topLevelDefaults, {
computed: {
origin,
pathname,
fullBaseUrl: joinURL(origin ?? '', pathname)
}
}),
const options = defu({
// We use `as` to infer backend types correctly for runtime-usage (everything is set, although for user everything was optional)
provider: defu(
userOptions.provider,
defaultsByBackend[selectedProvider]
) as DeepRequired<AuthProviders>
}
}, userOptions, topLevelDefaults)

// 1. Check if module should be enabled at all
if (!options.isEnabled) {
Expand All @@ -136,15 +128,23 @@ export default defineNuxtModule<ModuleOptions>({

logger.info('`nuxt-auth` setup starting')

// 2. Set up runtime configuration
// 2.1. Disable internal routing for `local` provider when not specified otherwise
// https://github.com/sidebase/nuxt-auth/issues/797
if (userOptions.disableInternalRouting === undefined && selectedProvider === 'local') {
options.disableInternalRouting = true
}

// 2.2. Set up runtime configuration
if (!isProduction) {
const authjsAddition
= selectedProvider === 'authjs'
? ', ensure that `NuxtAuthHandler({ ... })` is there, see https://sidebase.io/nuxt-auth/configuration/nuxt-auth-handler'
: ''
logger.info(
`Selected provider: ${selectedProvider}. Auth API location is \`${options.computed.fullBaseUrl}\`${authjsAddition}`
)
const loggerMessages = [
`Selected provider: ${selectedProvider}.`,
`Auth API location is \`${options.baseURL}\`, if you would like to change this, see https://auth.sidebase.io/guide/application-side/configuration#baseurl.`
]
if (selectedProvider === 'authjs') {
loggerMessages.push('Ensure that the `NuxtAuthHandler({ ... })` is there, see https://auth.sidebase.io/guide/authjs/nuxt-auth-handler')
}

logger.info(loggerMessages.join(' '))
}

nuxt.options.runtimeConfig = nuxt.options.runtimeConfig || { public: {} }
Expand Down Expand Up @@ -241,8 +241,8 @@ export default defineNuxtModule<ModuleOptions>({

// 6. Register middleware for autocomplete in definePageMeta
addRouteMiddleware({
name: 'auth',
path: resolve('./runtime/middleware/auth')
name: 'sidebase-auth',
path: resolve('./runtime/middleware/sidebase-auth')
})

// 7. Add plugin for initial load
Expand Down
23 changes: 13 additions & 10 deletions src/runtime/composables/authjs/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { AppProvider, BuiltInProviderType } from '@auth/core/providers/inde
import { defu } from 'defu'
import { type Ref, readonly } from 'vue'
import { appendHeader } from 'h3'
import { determineCallbackUrl } from '../../utils/url'
import { getRequestURLWN, joinPathToApiURLWN, makeCWN, navigateToAuthPageWN } from '../../utils/callWithNuxt'
import { determineCallbackUrl, resolveApiUrlPath } from '../../utils/url'
import { _fetch } from '../../utils/fetch'
import { isNonEmptyObject } from '../../utils/checkSessionResult'
import type { CommonUseAuthReturn, GetSessionOptions, SignInFunc, SignOutFunc } from '../../types'
import { useTypedBackendConfig } from '../../helpers'
import { getRequestURLWN } from '../common/getRequestURL'
import type { SessionData } from './useAuthState'
import { navigateToAuthPageWN } from './utils/navigateToAuthPage'
import type { NuxtApp } from '#app/nuxt'
import { callWithNuxt } from '#app/nuxt'
import { createError, useAuthState, useNuxtApp, useRequestHeaders, useRuntimeConfig } from '#imports'
Expand Down Expand Up @@ -49,7 +50,9 @@ async function getCsrfToken() {
const headers = await getRequestCookies(nuxt)
return _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers }).then(response => response.csrfToken)
}
const getCsrfTokenWithNuxt = makeCWN(getCsrfToken)
function getCsrfTokenWithNuxt(nuxt: NuxtApp) {
return callWithNuxt(nuxt, getCsrfToken)
}

/**
* Trigger a sign in flow for the passed `provider`. If no provider is given the sign in page for all providers will be shown.
Expand All @@ -61,17 +64,16 @@ const getCsrfTokenWithNuxt = makeCWN(getCsrfToken)
type SignInResult = void | { error: string | null, status: number, ok: boolean, url: any }
const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, options, authorizationParams) => {
const nuxt = useNuxtApp()
const runtimeConfig = await callWithNuxt(nuxt, useRuntimeConfig)

// 1. Lead to error page if no providers are available
const configuredProviders = await getProviders()
if (!configuredProviders) {
const errorUrl = await joinPathToApiURLWN(nuxt, 'error')
const errorUrl = resolveApiUrlPath('error', runtimeConfig)
return navigateToAuthPageWN(nuxt, errorUrl)
}

// 2. If no `provider` was given, either use the configured `defaultProvider` or `undefined` (leading to a forward to the `/login` page with all providers)
const runtimeConfig = await callWithNuxt(nuxt, useRuntimeConfig)

const backendConfig = useTypedBackendConfig(runtimeConfig, 'authjs')
if (typeof provider === 'undefined') {
// NOTE: `provider` might be an empty string
Expand All @@ -87,7 +89,7 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op
callbackUrl = await determineCallbackUrl(runtimeConfig.public.auth, () => getRequestURLWN(nuxt))
}

const signinUrl = await joinPathToApiURLWN(nuxt, 'signin')
const signinUrl = resolveApiUrlPath('signin', runtimeConfig)

const queryParams = callbackUrl ? `?${new URLSearchParams({ callbackUrl })}` : ''
const hrefSignInAllProviderPage = `${signinUrl}${queryParams}`
Expand Down Expand Up @@ -143,7 +145,6 @@ const signIn: SignInFunc<SupportedProviders, SignInResult> = async (provider, op

// At this point the request succeeded (i.e., it went through)
const error = new URL(data.url).searchParams.get('error')
// eslint-disable-next-line ts/no-use-before-define
await getSessionWithNuxt(nuxt)

return {
Expand All @@ -166,7 +167,7 @@ function getProviders() {
*
* @param getSessionOptions - Options for getting the session, e.g., set `required: true` to enforce that a session _must_ exist, the user will be directed to a login page otherwise.
*/
async function getSession(getSessionOptions?: GetSessionOptions): Promise<SessionData> {
async function getSession(getSessionOptions?: GetSessionOptions): Promise<SessionData | null> {
const nuxt = useNuxtApp()

const callbackUrlFallback = await getRequestURLWN(nuxt)
Expand Down Expand Up @@ -225,7 +226,9 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise<Sessio
headers
})
}
const getSessionWithNuxt = makeCWN(getSession)
function getSessionWithNuxt(nuxt: NuxtApp) {
return callWithNuxt(nuxt, getSession)
}

/**
* Sign out the current user.
Expand Down
53 changes: 53 additions & 0 deletions src/runtime/composables/authjs/utils/navigateToAuthPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { sanitizeStatusCode } from 'h3'
import { type NuxtApp, abortNavigation, callWithNuxt, useNuxtApp } from '#app'

export function navigateToAuthPageWN(nuxt: NuxtApp, href: string) {
return callWithNuxt(nuxt, navigateToAuthPage, [href])
}

/**
* Function to correctly navigate to auth-routes, necessary as the auth-routes are not part of the nuxt-app itself, so unknown to nuxt / vue-router.
*
* More specifically, we need this function to correctly handle the following cases:
* 1. On the client-side, returning `navigateTo(signInUrl)` leads to a `404` error as the next-auth-signin-page was not registered with the vue-router that is used for routing under the hood. For this reason we need to
* manually set `window.location.href` on the client **and then fake return a Promise that does not immediately resolve to block navigation (although it will not actually be fully awaited, but just be awaited long enough for the naviation to complete)**.
* 2. Additionally on the server-side, we cannot use `navigateTo(signInUrl)` as this uses `vue-router` internally which does not know the "external" sign-in page of next-auth and thus will log a warning which we want to avoid.
*
* Adapted from: https://github.com/nuxt/nuxt/blob/d188542a35bb541c7ed2e4502c687c2132979882/packages/nuxt/src/app/composables/router.ts#L161-L188
*
* @param href HREF / URL to navigate to
*/
export function navigateToAuthPage(href: string) {
const nuxtApp = useNuxtApp()

if (import.meta.server) {
if (nuxtApp.ssrContext) {
// TODO: consider deprecating in favour of `app:rendered` and removing
return nuxtApp.callHook('app:redirected').then(() => {
const encodedLoc = href.replace(/"/g, '%22')
const encodedHeader = new URL(href).toString()
nuxtApp.ssrContext!._renderResponse = {
statusCode: sanitizeStatusCode(302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader },
}
abortNavigation()
})
}
}

window.location.href = href
// If href contains a hash, the browser does not reload the page. We reload manually.
if (href.includes('#')) {
window.location.reload()
}

// TODO: Sadly, we cannot directly import types from `vue-router` as it leads to build failures. Typing the router about should help us to avoid manually typing `route` below
const router = nuxtApp.$router as { push: (href: string) => void }

// Wait for the `window.location.href` navigation from above to complete to avoid showing content. If that doesn't work fast enough, delegate navigation back to the `vue-router` (risking a vue-router 404 warning in the console, but still avoiding content-flashes of the protected target page)
const waitForNavigationWithFallbackToRouter = new Promise(resolve => setTimeout(resolve, 60 * 1000))
.then(() => router.push(href))

return waitForNavigationWithFallbackToRouter as Promise<void | undefined>
}
Loading

0 comments on commit 01fecc5

Please sign in to comment.