diff --git a/.changeset/breezy-mails-wave.md b/.changeset/breezy-mails-wave.md new file mode 100644 index 000000000..411e7dd63 --- /dev/null +++ b/.changeset/breezy-mails-wave.md @@ -0,0 +1,5 @@ +--- +'@hono/auth-js': major +--- + +Due to security concerns, support for `x-forward-*` has been removed starting from version 1.0.15. This change is marked as a breaking change. For more details, refer to issue [#817](https://github.com/honojs/middleware/issues/817). diff --git a/packages/auth-js/README.md b/packages/auth-js/README.md index 3457bfaa8..06848972a 100644 --- a/packages/auth-js/README.md +++ b/packages/auth-js/README.md @@ -16,7 +16,6 @@ Before starting using the middleware you must set the following environment vari ```plain AUTH_SECRET=#required -AUTH_URL=https://example.com/api/auth ``` ## How to Use @@ -38,6 +37,8 @@ app.use( clientSecret: c.env.GITHUB_SECRET, }), ], + // This is required and should match the path of the authHandler route + basePath: '/api/auth', })) ) @@ -117,3 +118,11 @@ For more details on how to Popup Oauth Login see [example](https://github.com/di ## Author Divyam + +### Contributors + +Catnaut + +## Breaking Changes + +Due to security concerns, support for `x-forward-*` has been removed starting from version 1.0.15. This change is marked as a breaking change. For more details, refer to issue [#817](https://github.com/honojs/middleware/issues/817). diff --git a/packages/auth-js/src/index.ts b/packages/auth-js/src/index.ts index cfda40c05..0a60ce11b 100644 --- a/packages/auth-js/src/index.ts +++ b/packages/auth-js/src/index.ts @@ -1,12 +1,12 @@ import type { AuthConfig as AuthConfigCore } from '@auth/core' -import { Auth } from '@auth/core' +import { Auth, createActionURL } from '@auth/core' import type { AdapterUser } from '@auth/core/adapters' import type { JWT } from '@auth/core/jwt' import type { Session } from '@auth/core/types' import type { Context, MiddlewareHandler } from 'hono' import { env } from 'hono/adapter' import { HTTPException } from 'hono/http-exception' -import { setEnvDefaults as coreSetEnvDefaults } from '@auth/core' +import { setEnvDefaults } from '@auth/core' declare module 'hono' { interface ContextVariableMap { @@ -32,80 +32,46 @@ export interface AuthConfig extends Omit {} export type ConfigHandler = (c: Context) => AuthConfig -export function setEnvDefaults(env: AuthEnv, config: AuthConfig) { - config.secret ??= env.AUTH_SECRET - coreSetEnvDefaults(env, config) -} - -export function reqWithEnvUrl(req: Request, authUrl?: string) { - if (authUrl) { - const reqUrlObj = new URL(req.url) - const authUrlObj = new URL(authUrl) - const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const - for (const prop of props) { - if (authUrlObj[prop]) reqUrlObj[prop] = authUrlObj[prop] - } - return new Request(reqUrlObj.href, req) - } - const url = new URL(req.url) - const newReq = new Request(url.href, req) - const proto = newReq.headers.get('x-forwarded-proto') - const host = newReq.headers.get('x-forwarded-host') ?? newReq.headers.get('host') - if (proto != null) url.protocol = proto.endsWith(':') ? proto : `${proto}:` - if (host != null) { - url.host = host - const portMatch = host.match(/:(\d+)$/) - if (portMatch) url.port = portMatch[1] - else url.port = '' - newReq.headers.delete('x-forwarded-host') - newReq.headers.delete('Host') - newReq.headers.set('Host', host) - } - return new Request(url.href, newReq) -} - -export async function getAuthUser(c: Context): Promise { +export async function getAuthUser(c: Context) { + const ctxEnv = env(c) const config = c.get('authConfig') - const ctxEnv = env(c) as AuthEnv - setEnvDefaults(ctxEnv, config) - const authReq = reqWithEnvUrl(c.req.raw, ctxEnv.AUTH_URL) - const origin = new URL(authReq.url).origin - const request = new Request(`${origin}${config.basePath}/session`, { - headers: { cookie: c.req.header('cookie') ?? '' }, - }) + const req = c.req.raw + + const url = createActionURL('session', new URL(req.url).protocol, req.headers, ctxEnv, config) let authUser: AuthUser = {} as AuthUser - const response = (await Auth(request, { - ...config, - callbacks: { - ...config.callbacks, - async session(...args) { - authUser = args[0] - const session = (await config.callbacks?.session?.(...args)) ?? args[0].session - const user = args[0].user ?? args[0].token - return { user, ...session } satisfies Session + const response = await Auth( + new Request(url, { headers: { cookie: req.headers.get('cookie') ?? '' } }), + { + ...config, + callbacks: { + ...config.callbacks, + async session(...args) { + authUser = args[0] + const session = (await config.callbacks?.session?.(...args)) ?? args[0].session + const user = args[0].user ?? args[0].token + return { user, ...session } satisfies Session + }, }, - }, - })) as Response + } + ) const session = (await response.json()) as Session | null return session?.user ? authUser : null } +/** + * A utility middleware to verify the session of the incoming request by getAuthUser under the hood. + * If unauthorized, it will throw a 401 Unauthorized error. + */ export function verifyAuth(): MiddlewareHandler { return async (c, next) => { - const authUser = await getAuthUser(c) + const authUser = c.get('authUser') ?? (await getAuthUser(c)) const isAuth = !!authUser?.token || !!authUser?.user - if (!isAuth) { - const res = new Response('Unauthorized', { - status: 401, - }) - throw new HTTPException(401, { res }) - } + if (!isAuth) throw new HTTPException(401, { message: 'Unauthorized' }) c.set('authUser', authUser) - await next() } } @@ -113,6 +79,8 @@ export function verifyAuth(): MiddlewareHandler { export function initAuthConfig(cb: ConfigHandler): MiddlewareHandler { return async (c, next) => { const config = cb(c) + const ctxEnv = env(c) as AuthEnv + setEnvDefaults(ctxEnv, config) c.set('authConfig', config) await next() } @@ -121,15 +89,7 @@ export function initAuthConfig(cb: ConfigHandler): MiddlewareHandler { export function authHandler(): MiddlewareHandler { return async (c) => { const config = c.get('authConfig') - const ctxEnv = env(c) as AuthEnv - - setEnvDefaults(ctxEnv, config) - - if (!config.secret || config.secret.length === 0) { - throw new HTTPException(500, { message: 'Missing AUTH_SECRET' }) - } - - const res = await Auth(reqWithEnvUrl(c.req.raw, ctxEnv.AUTH_URL), config) + const res = await Auth(c.req.raw, config) return new Response(res.body, res) } } diff --git a/packages/auth-js/test/index.test.ts b/packages/auth-js/test/index.test.ts index 15b89576f..befbbeae7 100644 --- a/packages/auth-js/test/index.test.ts +++ b/packages/auth-js/test/index.test.ts @@ -5,14 +5,14 @@ import Credentials from '@auth/core/providers/credentials' import { Hono } from 'hono' import { describe, expect, it, vi } from 'vitest' import type { AuthConfig } from '../src' -import { authHandler, verifyAuth, initAuthConfig, reqWithEnvUrl } from '../src' +import { authHandler, verifyAuth, initAuthConfig } from '../src' // @ts-expect-error - global crypto //needed for node 18 and below but should work in node 20 and above global.crypto = webcrypto describe('Config', () => { - it('Should return 500 if AUTH_SECRET is missing', async () => { + test('Should return 500 if AUTH_SECRET is missing', async () => { globalThis.process.env = { AUTH_SECRET: '' } const app = new Hono() @@ -20,6 +20,7 @@ describe('Config', () => { '/*', initAuthConfig(() => { return { + basePath: "/api/auth", providers: [], } }) @@ -28,7 +29,7 @@ describe('Config', () => { const req = new Request('http://localhost/api/auth/signin') const res = await app.request(req) expect(res.status).toBe(500) - expect(await res.text()).toBe('Missing AUTH_SECRET') + expect(await res.text()).include("There is a problem with the server configuration.") }) it('Should return 200 auth initial config is correct', async () => { @@ -51,6 +52,25 @@ describe('Config', () => { expect(res.status).toBe(200) }) + it('Should return 200 auth initial config is correct with AUTH_URL', async () => { + globalThis.process.env = { AUTH_SECRET: 'secret',AUTH_URL:'http://example.com/api/auth' } + const app = new Hono() + + app.use( + '/*', + initAuthConfig(() => { + return { + providers: [], + } + }) + ) + + app.use('/api/auth/*', authHandler()) + const req = new Request('http://localhost/api/auth/signin') + const res = await app.request(req) + expect(res.status).toBe(200) + }) + it('Should return 401 is if auth cookie is invalid or missing', async () => { const app = new Hono() @@ -79,13 +99,6 @@ describe('Config', () => { }) }) -describe('reqWithEnvUrl()', async () => { - const req = new Request('http://request-base/request-path') - const newReq = await reqWithEnvUrl(req, 'https://auth-url-base/auth-url-path') - it('Should rewrite the base path', () => { - expect(newReq.url.toString()).toBe('https://auth-url-base/request-path') - }) -}) describe('Credentials Provider', () => { const mockAdapter: Adapter = { @@ -221,14 +234,4 @@ describe('Credentials Provider', () => { expect(obj.data.name).toBe(data.name) }) - it('Should respect x-forwarded-proto and x-forwarded-host', async () => { - const headers = new Headers() - headers.append('x-forwarded-proto', 'https') - headers.append('x-forwarded-host', 'example.com') - const res = await app.request('http://localhost/api/auth/signin', { - headers, - }) - const html = await res.text() - expect(html).toContain('action="https://example.com/api/auth/callback/credentials"') - }) })