Skip to content

Commit

Permalink
feat: 增加authjs内部csrf验证
Browse files Browse the repository at this point in the history
  • Loading branch information
gxmari007 committed Jul 25, 2024
1 parent 794b8a8 commit b815237
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 87 deletions.
1 change: 0 additions & 1 deletion playground-authjs/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<button @click="signOut()">sign out</button>
</div>
{{ sessionData }}
<nuxt-page></nuxt-page>
</div>
</template>

Expand Down
10 changes: 8 additions & 2 deletions playground-authjs/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ['../src/module'],

auth: {
baseURL: 'http://localhost:3000/api/auth',
baseURL: '/api/auth',
provider: {
type: 'authjs',
},
globalAppMiddleware: true,
globalAppMiddleware: {
allow404WithoutAuth: false,
isEnabled: true,
},
},

compatibilityDate: '2024-07-25',
})
3 changes: 0 additions & 3 deletions playground-authjs/pages/index.vue

This file was deleted.

4 changes: 2 additions & 2 deletions playground-authjs/server/api/auth/[...].ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export const options: AuthConfig = {
secret: 'NtoBpkbWs4uTqsEvBJ/UyvWGFYAXQ9/tdNrmQCf7NIQ=',
providers: [
GitHub({
clientId: '10b3f386a001067199c0',
clientSecret: '85bca5873bed7f8744d88917e639518a3cbc3b90',
clientId: 'Ov23liQRrLjGUGn1YrGg',
clientSecret: '518ce6409b3ca4defe6b6b356939d2d462f23b41',
}),
],
}
Expand Down
5 changes: 1 addition & 4 deletions playground-authjs/server/api/session.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { options } from './auth/[...]'
import { getServerSession, getServerToken } from '#auth'
import { getServerSession } from '#auth'

export default defineEventHandler(async (event) => {
const session = await getServerSession(event)
const token = await getServerToken(event, options)

return {
session,
token,
}
})
4 changes: 4 additions & 0 deletions playground-local/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export default defineNuxtConfig({
modules: ['../src/module'],
devtools: { enabled: true },

build: {
transpile: ['jsonwebtoken'],
},

auth: {
baseURL: '/api/auth',
provider: {
Expand All @@ -22,4 +24,6 @@ export default defineNuxtConfig({
},
globalAppMiddleware: true,
},

compatibilityDate: '2024-07-25',
})
1 change: 0 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ export default defineNuxtModule<ModuleOptions>({
'./runtime/server/authjs',
)}').NuxtAuthHandler`,
` const getServerSession: typeof import('${resolve('./runtime/server/authjs')}').getServerSession`,
` const getServerToken: typeof import('${resolve('./runtime/server/authjs')}').getServerToken`,
].join('\n')
: genInterface('SessionData', options.provider.sessionData.type),
'}',
Expand Down
17 changes: 16 additions & 1 deletion src/runtime/composables/authjs/use-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ export const signIn = async (
return await nuxtApp.runWithContext(() => navigateToAuthPage(signInHref))
}

// 4. 获取 csrf token
const { csrfToken } = await request<{ csrfToken: string }>(nuxtApp, '/csrf')

if (!csrfToken) {
throw new Error('CSRF token not found')
}

// 4. 发起 signin 请求
const isCredentials = selectedProvider.type === 'credentials'
const isEmail = selectedProvider.type === 'email'
Expand All @@ -126,6 +133,7 @@ export const signIn = async (
// @ts-ignore
const params = new URLSearchParams({
...signInOptions,
csrfToken,
callbackUrl,
})

Expand Down Expand Up @@ -160,13 +168,19 @@ const signOut = async (options?: SignOutOptions) => {
const requestUrl = getRequestUrl()
const { redirect = true, callbackUrl = requestUrl } = options ?? {}

const { csrfToken } = await request<{ csrfToken: string }>(nuxtApp, '/csrf')

if (!csrfToken) {
throw new Error('CSRF token not found')
}

const response = await request<{ url: string }>(nuxtApp, '/signout', {
method: 'post',
headers: {
'content-type': 'application/x-www-form-urlencoded',
'x-auth-return-redirect': '1',
},
body: new URLSearchParams({ callbackUrl }),
body: new URLSearchParams({ csrfToken, callbackUrl }),
})

if (redirect) {
Expand All @@ -175,6 +189,7 @@ const signOut = async (options?: SignOutOptions) => {
}

await getSession()

return response
}

Expand Down
2 changes: 2 additions & 0 deletions src/runtime/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export default defineNuxtRouteMiddleware((to, from) => {
}

// 5. to 的路由如果为 404 状态需要忽略跳转 sign in 页面
// 没有路由的项目去要设置 allow404WithoutAuth 为 false
// 不然这里会认为默认页面属于 404 导致无法触发认证
if (
authConfig.globalAppMiddleware.allow404WithoutAuth ||
(typeof authConfig.globalAppMiddleware === 'boolean' && authConfig.globalAppMiddleware === true)
Expand Down
112 changes: 58 additions & 54 deletions src/runtime/plugins/auth.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,76 @@
import authMiddleware from '../middleware/auth'
import { addRouteMiddleware, defineNuxtPlugin, useAuth, useRuntimeConfig } from '#imports'

export default defineNuxtPlugin(async (nuxtApp) => {
// 1. 初始化变量,调用 getSession 获取用户权限数据
const config = useRuntimeConfig().public.auth
const { data, lastRefreshedAt, getSession } = useAuth()
export default defineNuxtPlugin({
name: '@roshan-labs/auth',
enforce: 'pre',
async setup(nuxtApp) {
// 1. 初始化变量,调用 getSession 获取用户权限数据
const config = useRuntimeConfig().public.auth
const { data, lastRefreshedAt, getSession } = useAuth()

if (data.value === null) {
await getSession()
}
if (data.value === null) {
await getSession()
}

// 2. 配置 session 刷新规则
let refetchIntervalTimer: number | null = null
const { enableRefreshPeriodically, enableRefreshOnWindowFocus } = config.session
// 2. 配置 session 刷新规则
let refetchIntervalTimer: number | null = null
const { enableRefreshPeriodically, enableRefreshOnWindowFocus } = config.session

// 如果配置 enableRefreshOnWindowFocus 为 true,则在每次 window 激活的时候重新获取 session
const onVisibilitychange = () => {
if (document.visibilityState === 'visible') {
getSession()
// 如果配置 enableRefreshOnWindowFocus 为 true,则在每次 window 激活的时候重新获取 session
const onVisibilitychange = () => {
if (document.visibilityState === 'visible') {
getSession()
}
}
}

nuxtApp.hook('app:mounted', () => {
if (enableRefreshOnWindowFocus) {
document.addEventListener('visibilitychange', onVisibilitychange, false)
}
nuxtApp.hook('app:mounted', () => {
if (enableRefreshOnWindowFocus) {
document.addEventListener('visibilitychange', onVisibilitychange, false)
}

// 设置定时刷新 session
if (enableRefreshPeriodically !== false) {
const intervalTime: number =
typeof enableRefreshPeriodically === 'boolean' ? 1000 : enableRefreshPeriodically
// 设置定时刷新 session
if (enableRefreshPeriodically !== false) {
const intervalTime: number =
typeof enableRefreshPeriodically === 'boolean' ? 1000 : enableRefreshPeriodically

refetchIntervalTimer = window.setInterval(() => {
if (data.value) {
getSession()
}
}, intervalTime)
}
})
refetchIntervalTimer = window.setInterval(() => {
if (data.value) {
getSession()
}
}, intervalTime)
}
})

// 在应用卸载时清理 session 相关事件
const _unmount = nuxtApp.vueApp.unmount
// 在应用卸载时清理 session 相关事件
const _unmount = nuxtApp.vueApp.unmount

nuxtApp.vueApp.unmount = () => {
// 清除 visibilitychange 事件
if (enableRefreshOnWindowFocus) {
document.removeEventListener('visibilitychange', onVisibilitychange)
}
nuxtApp.vueApp.unmount = () => {
// 清除 visibilitychange 事件
if (enableRefreshOnWindowFocus) {
document.removeEventListener('visibilitychange', onVisibilitychange)
}

// 清除刷新 session 定时器
if (refetchIntervalTimer !== null) {
window.clearInterval(refetchIntervalTimer)
}
// 清除刷新 session 定时器
if (refetchIntervalTimer !== null) {
window.clearInterval(refetchIntervalTimer)
}

// 清除 session
data.value = null
lastRefreshedAt.value = null
// 清除 session
data.value = null
lastRefreshedAt.value = null

_unmount()
}
_unmount()
}

// 3. 按配置判断是否注册全局路由中间件
const { globalAppMiddleware } = config
// 3. 按配置判断是否注册全局路由中间件
const { globalAppMiddleware } = config

if (
(typeof globalAppMiddleware === 'boolean' && globalAppMiddleware === true) ||
globalAppMiddleware.isEnabled
) {
addRouteMiddleware('auth', authMiddleware, { global: true })
}
if (
(typeof globalAppMiddleware === 'boolean' && globalAppMiddleware === true) ||
globalAppMiddleware.isEnabled
) {
addRouteMiddleware('auth', authMiddleware, { global: true })
}
},
})
41 changes: 22 additions & 19 deletions src/runtime/server/authjs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { AuthConfig, Session } from '@auth/core/types'
import { Auth, skipCSRFCheck } from '@auth/core'
import { getToken } from '@auth/core/jwt'
import { type H3Event, defineEventHandler, getRequestHeaders, parseCookies } from 'h3'
import { Auth, setEnvDefaults } from '@auth/core'
import { type H3Event, defineEventHandler, getRequestHeaders } from 'h3'
import { joinURL } from 'ufo'

import { getAuthSecret, getServerOrigin, getWebRequest } from '../utils/server'
import { getWebRequest } from '../utils/server'
import { useRuntimeConfig } from '#imports'

// Node.js 20 之后版本才有 globalThis.crypto,需要做兼容
Expand All @@ -20,16 +19,20 @@ if (!globalThis.crypto) {

export const NuxtAuthHandler = (options: AuthConfig) => {
return defineEventHandler(async (event) => {
// 忽略 sourcemap 文件
if (event.node.req.url?.includes('.js.map')) return
// 忽略 prerender 请求
if (event.node.req.headers?.['x-nitro-prerender'] && import.meta.env.NODE_ENV === 'prerender')
return

const request = await getWebRequest(event)

// 默认信任 host
options.trustHost ??= true
// 已经实现 csrf 检查,忽略 authjs 内部检查
options.skipCSRFCheck = skipCSRFCheck
// 设置 basePath
options.basePath = useRuntimeConfig().public.auth.params.pathname
options.basePath ??= useRuntimeConfig().public.auth.params.pathname

if (request.url.includes('.js.map')) return
setEnvDefaults(process.env, options)

return await Auth(request, options)
})
Expand All @@ -55,14 +58,14 @@ export const getServerSession = async (event: H3Event) => {
}
}

export const getServerToken = (event: H3Event, options: AuthConfig) => {
return getToken({
req: {
// @ts-ignore
cookies: parseCookies(event),
headers: getRequestHeaders(event) as Record<string, string>,
},
secret: getAuthSecret(options),
secureCookie: getServerOrigin(event, useRuntimeConfig()).startsWith('https://'),
})
}
// export const getServerToken = (event: H3Event, options: AuthConfig) => {
// return getToken({
// req: {
// // @ts-ignore
// cookies: parseCookies(event),
// headers: getRequestHeaders(event) as Record<string, string>,
// },
// secret: getAuthSecret(options),
// secureCookie: getServerOrigin(event, useRuntimeConfig()).startsWith('https://'),
// })
// }
10 changes: 10 additions & 0 deletions src/runtime/utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,13 @@ export const getServerOrigin = (event: H3Event, runtimeConfig: RuntimeConfig) =>

return origin
}

/**
* 获取 request path,去除尾部 /
*
* @param req Request 请求
* @returns 格式化后的 request path
*/
export const getBasePath = (req: Request) => {
return req.url.replace(/\/$/, '')
}

0 comments on commit b815237

Please sign in to comment.