Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,11 @@ jobs:
- name: Install deps
run: pnpm i

- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps

# Check building
- run: pnpm build
env:
PORT: 3001

# start prod-app and curl from it
- run: "timeout 60 pnpm start & (sleep 45 && curl --fail localhost:$PORT)"
env:
AUTH_ORIGIN: "http://localhost:3001/api/auth"
PORT: 3001
- name: Run Playwright tests
run: pnpm test:e2e
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ npx nuxi@latest module add sidebase-auth
<details>
<summary>Or install manually</summary>

#### 1. Install the package as a dev dependency
### 1. Install the package as a dev dependency

```sh
npm i -D @sidebase/nuxt-auth
Expand All @@ -49,7 +49,7 @@ npx nuxi@latest module add sidebase-auth
yarn add --dev @sidebase/nuxt-auth
```

#### 2. Add the modules to your `nuxt.config.ts`
### 2. Add the modules to your `nuxt.config.ts`

```ts
export default defineNuxtConfig({
Expand Down Expand Up @@ -174,5 +174,5 @@ Thank you to everyone who has contributed to this project by writing issues or o
`@sidebase/nuxt-auth` is supported by all of our amazing contributors and the [Nuxt 3+ team](https://nuxters.nuxt.com/)!

<a href="https://github.com/sidebase/nuxt-auth/graphs/contributors">
<img src="https://contrib.rocks/image?repo=sidebase/nuxt-auth" />
<img src="https://contrib.rocks/image?repo=sidebase/nuxt-auth" alt="Contributors" />
</a>
4 changes: 2 additions & 2 deletions docs/guide/getting-started/choose-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ In `v0.9.0` the `refresh` provider was integrated into the `local` provider. Rea

If you are still unsure, below are some tables to help you pick:

### Authentication Methods
## Authentication Methods

| | authjs provider | local provider
|----------------------------------------------------------- |-------------------------------------: |---------------:
Expand All @@ -24,7 +24,7 @@ If you are still unsure, below are some tables to help you pick:
| Credentials / Username + Password flow | 🚧 (if possible: use `local` instead) | βœ…
| Refresh tokens | βœ… | βœ…

### Features
## Features

| | authjs provider | local provider
|----------------------------------------------------------- |-------------------------------------: |------:
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/getting-started/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ NextAuth versions under `4.22` are impacted by vulnerability [GHSA-v64w-49xw-qq8

::: details Further details
---
#### Description of the vulnerability
### Description of the vulnerability
The vulnerability [GHSA-v64w-49xw-qq89](https://github.com/advisories/GHSA-v64w-49xw-qq89) only affects applications that rely on the default [Middleware authorization](https://next-auth.js.org/configuration/nextjs#middleware) provided by NextAuth.

The vulnerability allows attackers to create/mock a user, by accessing the JWT from an interrupted OAuth sign-in flow. They can then manually override the session cookie and simulate a login. However, doing this does **not** give access to the users data or permissions, but can allow attackers to view the layouts of protected pages.

#### Why does it not effect NuxtAuth?
### Why does it not affect NuxtAuth?
As the affected middleware is written for Next.js, we wrote our own [custom middleware](https://github.com/sidebase/nuxt-auth/blob/main/src/runtime/middleware/auth.ts) for NuxtAuth that is not affected by the vulnerability.
:::
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,20 @@
}
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.3",
"@antfu/eslint-config": "^7.7.3",
"@nuxt/module-builder": "^1.0.2",
"@nuxt/schema": "^3.20.2",
"@nuxtjs/eslint-config-typescript": "^12.1.0",
"@types/node": "^24.10.11",
"eslint": "^9.39.2",
"eslint": "^10.1.0",
"nuxt": "^3.20.2",
"ofetch": "^1.5.1",
"oxlint": "^1.39.0",
"oxlint": "^1.57.0",
"ts-essentials": "^9.4.2",
"typescript": "^5.8.3",
"vitepress": "^1.6.4",
"vitest": "^3.2.4",
"vue-tsc": "^2.2.12"
"vitest": "^4.1.2",
"vue-tsc": "^3.2.6"
},
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
}
14 changes: 9 additions & 5 deletions playground-authjs/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const route = useRoute()
<p>
See all available authentication & session information below. Navigate to different sub-pages to test out the app.
</p>
<pre>Status: {{ status }}</pre>
<pre>Status: <span data-testid="status">{{ status }}</span></pre>
<pre>Data: {{ data || 'no session data present, are you logged in?' }}</pre>
<pre>Last refreshed at: {{ lastRefreshedAt || 'no refresh happened' }}</pre>
<pre>Decoded JWT token: {{ token?.token || 'no token present, are you logged in?' }}</pre>
Expand All @@ -50,7 +50,7 @@ const route = useRoute()
<h2>Actions</h2>
<p>Take different actions:</p>
<div>
<button @click="signIn(undefined, { callbackUrl: '/' })">
<button data-testid="signin" @click="signIn(undefined, { callbackUrl: '/' })">
sign in
</button>
<br>
Expand All @@ -66,12 +66,16 @@ const route = useRoute()
sign in (with redirect to protected page)
</button>
<br>
<button @click="signOut({ callbackUrl: '/signout' })">
<button data-testid="signout" @click="signOut({ callbackUrl: '/signout' })">
sign out
</button>
<br>
<button @click="getSession({ required: false })">
refresh session
<button data-testid="refresh-required-false" @click="getSession({ required: false })">
refresh session (required: false)
</button>
<br>
<button data-testid="refresh-required-true" @click="getSession({ required: true, callbackUrl: '/' })">
refresh session (required: true)
</button>
</div>
<hr>
Expand Down
10 changes: 8 additions & 2 deletions playground-authjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
"dev": "nuxi prepare && nuxi dev",
"build": "nuxi build",
"start": "nuxi preview",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"test:e2e": "vitest"
},
"devDependencies": {
"@nuxt/test-utils": "^4.0.0",
"@playwright/test": "^1.58.2",
"@types/node": "^24.10.11",
"@vue/test-utils": "^2.4.6",
"nuxt": "^3.20.2",
"typescript": "^5.8.3",
"vue-tsc": "^2.2.12"
"vitest": "^4.1.2",
"vue-tsc": "^3.2.6"
}
}
11 changes: 8 additions & 3 deletions playground-authjs/pages/custom-signin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ const password = ref('')

const { signIn } = useAuth()

async function mySignInHandler({ username, password, callbackUrl }: { username: string, password: string, callbackUrl: string }) {
const { error, url } = await signIn('credentials', { username, password, callbackUrl, redirect: false })
async function mySignInHandler(callbackUrl: string) {
const { error, url } = await signIn('credentials', {
username: username.value,
password: password.value,
callbackUrl,
redirect: false
})

if (error) {
// Do your custom error handling here
Expand Down Expand Up @@ -39,7 +44,7 @@ async function mySignInHandler({ username, password, callbackUrl }: { username:
Sign in with username and password
</button>
<br>
<button @click="mySignInHandler({ username, password, callbackUrl: '/protected/globally' })">
<button @click="mySignInHandler('/protected/globally')">
Sign in with username and password using a custom sign in handler
</button>
</div>
Expand Down
77 changes: 77 additions & 0 deletions playground-authjs/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { defineConfig, devices } from '@playwright/test'

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:3000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
}

// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] }
// }

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
]

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
})
73 changes: 73 additions & 0 deletions playground-authjs/tests/authjs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createPage, setup } from '@nuxt/test-utils/e2e'
import { expect as playwrightExpect } from '@nuxt/test-utils/playwright'
import { describe, it } from 'vitest'

const STATUS_AUTHENTICATED = 'authenticated'
const STATUS_UNAUTHENTICATED = 'unauthenticated'

const BASE_URL = 'http://127.0.0.1:3000'

describe('authjs Provider', async () => {
await setup({
runner: 'vitest',
browser: true,
port: 3000,
env: {
AUTH_ORIGIN: `${BASE_URL}/api/auth`,
},
})

it('load, sign in, reload, refresh, sign out', async () => {
const page = await createPage('/', { baseURL: BASE_URL })

// Locators
const [
signInButton,
status,
signoutButton,
refreshRequiredFalseButton,
refreshRequiredTrueButton
] = await Promise.all([
page.getByTestId('signin'),
page.getByTestId('status'),
page.getByTestId('signout'),
page.getByTestId('refresh-required-false'),
page.getByTestId('refresh-required-true')
])

// Unauthenticated at first
await playwrightExpect(status).toHaveText(STATUS_UNAUTHENTICATED)

// Trigger normal signin page
await signInButton.click()
await playwrightExpect(page).toHaveURL(toUrl('/api/auth/signin?callbackUrl=%2F'))

// Fill username and password, submit
await page.getByPlaceholder('(hint: jsmith)').fill('jsmith')
await page.getByPlaceholder('(hint: hunter2)').fill('hunter2')
await page.getByRole('button', { name: 'Sign in with Credentials' }).click()

await playwrightExpect(page).toHaveURL(toUrl('/'))
await playwrightExpect(status).toHaveText(STATUS_AUTHENTICATED)

// Ensure that we are still authenticated after page refresh
await page.reload()
await playwrightExpect(status).toHaveText(STATUS_AUTHENTICATED)

// Refresh (required: false), status should not change
await refreshRequiredFalseButton.click()
await playwrightExpect(status).toHaveText(STATUS_AUTHENTICATED)

// Refresh (required: true), status should not change
await refreshRequiredTrueButton.click()
await playwrightExpect(status).toHaveText(STATUS_AUTHENTICATED)

// Sign out, status should change
await signoutButton.click()
await playwrightExpect(status).toHaveText(STATUS_UNAUTHENTICATED)
}, 30000)
})

function toUrl(path: string): string {
return new URL(path, BASE_URL).href
}
8 changes: 4 additions & 4 deletions playground-local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
"test:e2e": "vitest"
},
"dependencies": {
"jose": "^6.1.3",
"jose": "^6.2.2",
"zod": "^3.25.76"
},
"devDependencies": {
"@nuxt/test-utils": "^3.23.0",
"@nuxt/test-utils": "^4.0.0",
"@playwright/test": "^1.58.2",
"@types/node": "^24.10.11",
"@vue/test-utils": "^2.4.6",
"nuxt": "^3.20.2",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"vue-tsc": "^2.2.12"
"vitest": "^4.1.2",
"vue-tsc": "^3.2.6"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty big bump to vue-tsc. Are there any other packages that make sense to update too (e.g. jump to typescript 6?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Soooo? πŸ‘€

}
}
8 changes: 4 additions & 4 deletions playground-local/server/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface TokensByUser {
* Tokens storage.
* You will need to implement your own, connect with DB/etc.
*/
const tokensByUser: Map<string, TokensByUser> = new Map()
const tokensByUserMap: Map<string, TokensByUser> = new Map()

/**
* We use a fixed password for demo purposes.
Expand Down Expand Up @@ -72,13 +72,13 @@ export async function createUserTokens(user: User): Promise<UserTokens> {
const refreshToken = await createSignedJwt(tokenData, /* 1 day */ 60 * 60 * 24)

// Naive implementation - please implement properly yourself!
const userTokens: TokensByUser = tokensByUser.get(user.username) ?? {
const userTokens: TokensByUser = tokensByUserMap.get(user.username) ?? {
access: new Map(),
refresh: new Map()
}
userTokens.access.set(accessToken, refreshToken)
userTokens.refresh.set(refreshToken, accessToken)
tokensByUser.set(user.username, userTokens)
tokensByUserMap.set(user.username, userTokens)

return {
accessToken,
Expand All @@ -99,7 +99,7 @@ export async function decodeToken(token: string): Promise<JWTPayload> {
* Your implementation will likely never need this and will rely on User ID and DB.
*/
export function getTokensByUser(username: string): TokensByUser | undefined {
return tokensByUser.get(username)
return tokensByUserMap.get(username)
}

type CheckUserTokensResult = { valid: true, knownAccessToken: string } | { valid: false, knownAccessToken: undefined }
Expand Down
Loading
Loading