fix(auth-js): use `env` in `hono/adapter` and add tests (#486)
* fix(auth-js): use `env` in `hono/adapter` and add tests * add changeset * polyfill cryptopull/487/head
parent
173fed200a
commit
18959557f4
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@hono/auth-js': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: use `env` in `hono/adapter` and add tests
|
|
@ -57,7 +57,7 @@
|
||||||
"react": ">=18"
|
"react": ">=18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@auth/core": "^0.24.0",
|
"@auth/core": "^0.30.0",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"hono": "^3.11.7",
|
"hono": "^3.11.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { AdapterUser } from '@auth/core/adapters'
|
||||||
import type { JWT } from '@auth/core/jwt'
|
import type { JWT } from '@auth/core/jwt'
|
||||||
import type { Session } from '@auth/core/types'
|
import type { Session } from '@auth/core/types'
|
||||||
import type { Context, MiddlewareHandler } from 'hono'
|
import type { Context, MiddlewareHandler } from 'hono'
|
||||||
|
import { env } from 'hono/adapter'
|
||||||
import { HTTPException } from 'hono/http-exception'
|
import { HTTPException } from 'hono/http-exception'
|
||||||
|
|
||||||
declare module 'hono' {
|
declare module 'hono' {
|
||||||
|
@ -34,10 +35,10 @@ export function reqWithEnvUrl(req: Request, authUrl?: string): Request {
|
||||||
const reqUrlObj = new URL(req.url)
|
const reqUrlObj = new URL(req.url)
|
||||||
const authUrlObj = new URL(authUrl)
|
const authUrlObj = new URL(authUrl)
|
||||||
const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const
|
const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const
|
||||||
props.forEach(prop => reqUrlObj[prop] = authUrlObj[prop])
|
props.forEach((prop) => (reqUrlObj[prop] = authUrlObj[prop]))
|
||||||
return new Request(reqUrlObj.href, req);
|
return new Request(reqUrlObj.href, req)
|
||||||
} else {
|
} else {
|
||||||
return req;
|
return req
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +63,8 @@ function setEnvDefaults(env: AuthEnv, config: AuthConfig) {
|
||||||
|
|
||||||
export async function getAuthUser(c: Context): Promise<AuthUser | null> {
|
export async function getAuthUser(c: Context): Promise<AuthUser | null> {
|
||||||
const config = c.get('authConfig')
|
const config = c.get('authConfig')
|
||||||
setEnvDefaults(c.env, config)
|
setEnvDefaults(env(c), config)
|
||||||
const origin = c.env.AUTH_URL ? new URL(c.env.AUTH_URL).origin : new URL(c.req.url).origin
|
const origin = env(c)['AUTH_URL'] ? new URL(env(c)['AUTH_URL']).origin : new URL(c.req.url).origin
|
||||||
const request = new Request(`${origin}${config.basePath}/session`, {
|
const request = new Request(`${origin}${config.basePath}/session`, {
|
||||||
headers: { cookie: c.req.header('cookie') ?? '' },
|
headers: { cookie: c.req.header('cookie') ?? '' },
|
||||||
})
|
})
|
||||||
|
@ -117,13 +118,13 @@ export function authHandler(): MiddlewareHandler {
|
||||||
return async (c) => {
|
return async (c) => {
|
||||||
const config = c.get('authConfig')
|
const config = c.get('authConfig')
|
||||||
|
|
||||||
setEnvDefaults(c.env, config)
|
setEnvDefaults(env(c), config)
|
||||||
|
|
||||||
if (!config.secret) {
|
if (!config.secret) {
|
||||||
throw new HTTPException(500, { message: 'Missing AUTH_SECRET' })
|
throw new HTTPException(500, { message: 'Missing AUTH_SECRET' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await Auth(reqWithEnvUrl(c.req.raw, c.env.AUTH_URL), config)
|
const res = await Auth(reqWithEnvUrl(c.req.raw, env(c)['AUTH_URL']), config)
|
||||||
return new Response(res.body, res)
|
return new Response(res.body, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { webcrypto } from 'node:crypto'
|
import { webcrypto } from 'node:crypto'
|
||||||
|
import { skipCSRFCheck } from '@auth/core'
|
||||||
|
import type { Adapter } from '@auth/core/adapters'
|
||||||
|
import Credentials from '@auth/core/providers/credentials'
|
||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import type { AuthConfig } from '../src'
|
||||||
import { authHandler, verifyAuth, initAuthConfig, reqWithEnvUrl } from '../src'
|
import { authHandler, verifyAuth, initAuthConfig, reqWithEnvUrl } from '../src'
|
||||||
|
|
||||||
// @ts-expect-error - global crypto
|
// @ts-expect-error - global crypto
|
||||||
//needed for node 18 and below but should work in node 20 and above
|
//needed for node 18 and below but should work in node 20 and above
|
||||||
global.crypto = webcrypto
|
global.crypto = webcrypto
|
||||||
|
|
||||||
describe('Auth.js Adapter Middleware', () => {
|
describe('Config', () => {
|
||||||
it('Should return 500 if AUTH_SECRET is missing', async () => {
|
it('Should return 500 if AUTH_SECRET is missing', async () => {
|
||||||
|
globalThis.process.env = { AUTH_SECRET: '' }
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
||||||
app.use('/*', (c, next) => {
|
app.use('/*', (c, next) => {
|
||||||
|
@ -32,14 +37,10 @@ describe('Auth.js Adapter Middleware', () => {
|
||||||
expect(await res.text()).toBe('Missing AUTH_SECRET')
|
expect(await res.text()).toBe('Missing AUTH_SECRET')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return 200 auth initial config is correct', async () => {
|
it('Should return 200 auth initial config is correct', async () => {
|
||||||
|
globalThis.process.env = { AUTH_SECRET: 'secret' }
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
|
||||||
app.use('/*', (c, next) => {
|
|
||||||
c.env = { AUTH_SECRET: 'secret' }
|
|
||||||
return next()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/*',
|
'/*',
|
||||||
initAuthConfig(() => {
|
initAuthConfig(() => {
|
||||||
|
@ -90,3 +91,107 @@ describe('reqWithEnvUrl()', () => {
|
||||||
expect(newReq.url.toString()).toBe('https://auth-url-base/request-path')
|
expect(newReq.url.toString()).toBe('https://auth-url-base/request-path')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Credentials Provider', () => {
|
||||||
|
const mockAdapter: Adapter = {
|
||||||
|
createVerificationToken: vi.fn(),
|
||||||
|
useVerificationToken: vi.fn(),
|
||||||
|
getUserByEmail: vi.fn(),
|
||||||
|
createUser: vi.fn(),
|
||||||
|
getUser: vi.fn(),
|
||||||
|
getUserByAccount: vi.fn(),
|
||||||
|
updateUser: vi.fn(),
|
||||||
|
linkAccount: vi.fn(),
|
||||||
|
createSession: vi.fn(),
|
||||||
|
getSessionAndUser: vi.fn(),
|
||||||
|
updateSession: vi.fn(),
|
||||||
|
deleteSession: vi.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.process.env = {
|
||||||
|
AUTH_SECRET: 'secret',
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = { email: 'hono@hono.hono', name: 'Hono' }
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
app.use('*', initAuthConfig(getAuthConfig))
|
||||||
|
|
||||||
|
app.use('/api/auth/*', authHandler())
|
||||||
|
|
||||||
|
app.use('/api/*', verifyAuth())
|
||||||
|
|
||||||
|
app.get('/api/protected', (c) => {
|
||||||
|
const auth = c.get('authUser')
|
||||||
|
return c.json(auth)
|
||||||
|
})
|
||||||
|
|
||||||
|
const credentials = Credentials({
|
||||||
|
credentials: {
|
||||||
|
password: {},
|
||||||
|
},
|
||||||
|
authorize: (credentials) => {
|
||||||
|
if (credentials.password === 'password') {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function getAuthConfig(): AuthConfig {
|
||||||
|
return {
|
||||||
|
secret: 'secret',
|
||||||
|
providers: [credentials],
|
||||||
|
adapter: mockAdapter,
|
||||||
|
skipCSRFCheck,
|
||||||
|
callbacks: {
|
||||||
|
jwt: ({ token, user }) => {
|
||||||
|
if (user) {
|
||||||
|
token.id = user.id
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: 'jwt',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookie = ['']
|
||||||
|
|
||||||
|
it('Should not authorize and return 302 - /api/auth/callback/credentials', async () => {
|
||||||
|
const res = await app.request('/api/auth/callback/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(302)
|
||||||
|
expect(res.headers.get('location')).toBe(
|
||||||
|
'http://localhost/api/auth/signin?error=CredentialsSignin&code=credentials'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should authorize and return 302 - /api/auth/callback/credentials', async () => {
|
||||||
|
const res = await app.request('http://localhost/api/auth/callback/credentials', {
|
||||||
|
method: 'POST',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
password: 'password',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(302)
|
||||||
|
expect(res.headers.get('location')).toBe('http://localhost')
|
||||||
|
cookie = res.headers.getSetCookie()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should authorize and return 200 - /api/protected', async () => {
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.append('cookie', cookie[1])
|
||||||
|
const res = await app.request('http://localhost/api/protected', {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const obj = await res.json()
|
||||||
|
expect(obj['token']['name']).toBe(user.name)
|
||||||
|
expect(obj['token']['email']).toBe(user.email)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -45,9 +45,9 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@auth/core@npm:^0.24.0":
|
"@auth/core@npm:^0.30.0":
|
||||||
version: 0.24.0
|
version: 0.30.0
|
||||||
resolution: "@auth/core@npm:0.24.0"
|
resolution: "@auth/core@npm:0.30.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@panva/hkdf": "npm:^1.1.1"
|
"@panva/hkdf": "npm:^1.1.1"
|
||||||
"@types/cookie": "npm:0.6.0"
|
"@types/cookie": "npm:0.6.0"
|
||||||
|
@ -57,11 +57,17 @@ __metadata:
|
||||||
preact: "npm:10.11.3"
|
preact: "npm:10.11.3"
|
||||||
preact-render-to-string: "npm:5.2.3"
|
preact-render-to-string: "npm:5.2.3"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
"@simplewebauthn/browser": ^9.0.1
|
||||||
|
"@simplewebauthn/server": ^9.0.2
|
||||||
nodemailer: ^6.8.0
|
nodemailer: ^6.8.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
"@simplewebauthn/browser":
|
||||||
|
optional: true
|
||||||
|
"@simplewebauthn/server":
|
||||||
|
optional: true
|
||||||
nodemailer:
|
nodemailer:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: b8d8c66bc35d18a6ffa80e21b122747cb0c40826f68eb8c22a1b4dda01aba62c2050a1d5e4997e92a6756edad4103faafe8d9a49c7278d991e690c3ebbdb6035
|
checksum: caa94cc9b42c354fef57e337a844bca0c0770ac809ba1cf00b30d7fc2e383d1d42dafeb3e39b9dde92b85a9eb821cde905b662638bfe98d8e2439c6d3e64c8cd
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
@ -1779,7 +1785,7 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@hono/auth-js@workspace:packages/auth-js"
|
resolution: "@hono/auth-js@workspace:packages/auth-js"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@auth/core": "npm:^0.24.0"
|
"@auth/core": "npm:^0.30.0"
|
||||||
"@types/react": "npm:^18"
|
"@types/react": "npm:^18"
|
||||||
hono: "npm:^3.11.7"
|
hono: "npm:^3.11.7"
|
||||||
jest: "npm:^29.7.0"
|
jest: "npm:^29.7.0"
|
||||||
|
|
Loading…
Reference in New Issue