353 lines
8.8 KiB
TypeScript
353 lines
8.8 KiB
TypeScript
import { skipCSRFCheck } from '@auth/core'
|
|
import type { Adapter } from '@auth/core/adapters'
|
|
import Credentials from '@auth/core/providers/credentials'
|
|
import { Hono } from 'hono'
|
|
import { validator } from 'hono/validator'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { AuthConfig } from '.'
|
|
import { authHandler, verifyAuth, initAuthConfig, reqWithEnvUrl } from '.'
|
|
|
|
describe('Config', () => {
|
|
it('Should return 500 if AUTH_SECRET is missing', async () => {
|
|
globalThis.process.env = { AUTH_SECRET: '' }
|
|
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(500)
|
|
expect(await res.text()).toBe('Missing AUTH_SECRET')
|
|
})
|
|
|
|
it('Should return 200 auth initial config is correct', async () => {
|
|
globalThis.process.env = { AUTH_SECRET: 'secret' }
|
|
const app = new Hono()
|
|
|
|
app.use(
|
|
'/*',
|
|
initAuthConfig(() => {
|
|
return {
|
|
basePath: '/api/auth',
|
|
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()
|
|
|
|
app.use('/*', (c, next) => {
|
|
c.env = { AUTH_SECRET: 'secret' }
|
|
return next()
|
|
})
|
|
|
|
app.use(
|
|
'/*',
|
|
initAuthConfig(() => {
|
|
return {
|
|
providers: [],
|
|
}
|
|
})
|
|
)
|
|
|
|
app.use('/api/*', verifyAuth())
|
|
|
|
app.use('/api/auth/*', authHandler())
|
|
|
|
app.get('/api/protected', (c) => c.text('protected'))
|
|
const req = new Request('http://localhost/api/protected')
|
|
const res = await app.request(req)
|
|
expect(res.status).toBe(401)
|
|
})
|
|
})
|
|
|
|
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')
|
|
})
|
|
|
|
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 credentials = Credentials({
|
|
credentials: {
|
|
password: {},
|
|
},
|
|
authorize: (credentials) => {
|
|
if (credentials.password === 'password') {
|
|
return user
|
|
}
|
|
return null
|
|
},
|
|
})
|
|
|
|
function getAuthConfig(): AuthConfig {
|
|
return {
|
|
secret: 'secret',
|
|
providers: [credentials],
|
|
adapter: mockAdapter,
|
|
basePath: '/api/auth',
|
|
callbacks: {
|
|
jwt: ({ token, user }) => {
|
|
if (user) {
|
|
token.id = user.id
|
|
}
|
|
return token
|
|
},
|
|
},
|
|
session: {
|
|
strategy: 'jwt',
|
|
},
|
|
}
|
|
}
|
|
|
|
let cookie = ['']
|
|
|
|
it('Should be able to instantiate new Request after passing through validator', async () => {
|
|
const app = new Hono()
|
|
|
|
app.use('*', initAuthConfig(getAuthConfig))
|
|
app.use(
|
|
'/api/auth/*',
|
|
validator('form', (value, _) => {
|
|
const csrfToken = value['csrfToken']
|
|
return {
|
|
csrfToken,
|
|
}
|
|
}),
|
|
authHandler()
|
|
)
|
|
|
|
let csrfRes = await app.request('http://localhost/api/auth/csrf', {
|
|
method: 'GET',
|
|
})
|
|
let { csrfToken } = await csrfRes.json()
|
|
|
|
cookie = csrfRes.headers.getSetCookie()
|
|
|
|
let headers = new Headers()
|
|
headers.append('cookie', cookie[0])
|
|
|
|
const signInRes = await app.request('http://localhost/api/auth/callback/credentials', {
|
|
method: 'POST',
|
|
headers,
|
|
body: new URLSearchParams({
|
|
csrfToken,
|
|
password: 'password',
|
|
}),
|
|
})
|
|
expect(signInRes.status).toBe(302)
|
|
expect(signInRes.headers.get('location')).toBe('http://localhost')
|
|
|
|
cookie = signInRes.headers.getSetCookie()
|
|
|
|
const sessionCookie = cookie[1]
|
|
|
|
headers = new Headers()
|
|
headers.append('cookie', cookie[1])
|
|
headers.append('Content-Type', 'application/json')
|
|
|
|
csrfRes = await app.request('http://localhost/api/auth/csrf', {
|
|
method: 'GET',
|
|
})
|
|
;({ csrfToken } = await csrfRes.json())
|
|
|
|
cookie = csrfRes.headers.getSetCookie()
|
|
|
|
headers = new Headers()
|
|
headers.append('cookie', cookie[0])
|
|
headers.append('cookie', sessionCookie)
|
|
|
|
const req = new Request('http://localhost/api/auth/signout', {
|
|
method: 'POST',
|
|
body: new URLSearchParams({
|
|
csrfToken,
|
|
password: 'password',
|
|
}),
|
|
headers,
|
|
})
|
|
const res = await app.request(req)
|
|
expect(res.status).toBe(302)
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
|
|
app.post('/api/create', async (c) => {
|
|
const data = await c.req.json()
|
|
return c.json({ data })
|
|
})
|
|
|
|
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,
|
|
basePath: '/api/auth',
|
|
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()) as {
|
|
token: {
|
|
name: string
|
|
email: string
|
|
}
|
|
}
|
|
expect(obj.token.name).toBe(user.name)
|
|
expect(obj.token.email).toBe(user.email)
|
|
})
|
|
|
|
it('Should authorize and return 200 - /api/create', async () => {
|
|
const data = { name: 'Hono' }
|
|
|
|
const headers = new Headers()
|
|
headers.append('cookie', cookie[1])
|
|
headers.append('Content-Type', 'application/json')
|
|
const res = await app.request('http://localhost/api/create', {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(data),
|
|
})
|
|
expect(res.status).toBe(200)
|
|
const obj = (await res.json()) as {
|
|
data: {
|
|
name: string
|
|
}
|
|
}
|
|
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"')
|
|
})
|
|
})
|