honojs-middleware/packages/oauth-providers/test/index.test.ts

775 lines
22 KiB
TypeScript
Raw Normal View History

import { Hono } from 'hono'
import { setupServer } from 'msw/node'
import type { DiscordUser } from '../src/providers/discord'
import {
discordAuth,
refreshToken as discordRefresh,
revokeToken as discordRevoke,
} from '../src/providers/discord'
import { facebookAuth } from '../src/providers/facebook'
import type { FacebookUser } from '../src/providers/facebook'
import { githubAuth } from '../src/providers/github'
import type { GitHubUser } from '../src/providers/github'
import { googleAuth } from '../src/providers/google'
import type { GoogleUser } from '../src/providers/google'
import { linkedinAuth } from '../src/providers/linkedin'
import type { LinkedInUser } from '../src/providers/linkedin'
import type { XUser } from '../src/providers/x'
import { refreshToken, revokeToken, xAuth } from '../src/providers/x'
import type { Token } from '../src/types'
import {
discordCodeError,
discordRefreshToken,
discordRefreshTokenError,
discordToken,
discordUser,
dummyCode,
dummyToken,
facebookCodeError,
facebookUser,
githubCodeError,
githubToken,
githubUser,
googleCodeError,
googleUser,
handlers,
linkedInCodeError,
linkedInToken,
linkedInUser,
xCodeError,
xRefreshToken,
xRefreshTokenError,
xRevokeTokenError,
xToken,
xUser,
} from './handlers'
const server = setupServer(...handlers)
server.listen()
const client_id = '1jsdsldjkssd-4343dsasdsd34ghhn4-dummyid'
const client_secret = 'SDJS943hS_jj45dummysecret'
describe('OAuth Middleware', () => {
const app = new Hono()
2024-01-08 04:22:20 +08:00
// Google
app.use(
'/google',
googleAuth({
client_id,
client_secret,
scope: ['openid', 'email', 'profile'],
})
)
app.use('/google-custom-redirect', (c, next) => {
return googleAuth({
client_id,
client_secret,
scope: ['openid', 'email', 'profile'],
redirect_uri: 'http://localhost:3000/google',
})(c, next)
})
app.use('/google-custom-params', (c, next) => {
return googleAuth({
client_id,
client_secret,
scope: ['openid', 'email', 'profile'],
redirect_uri: 'http://localhost:3000/google',
login_hint: 'test-login-hint',
prompt: 'select_account',
state: 'test-state',
access_type: 'offline',
})(c, next)
})
app.get('/google', (c) => {
const user = c.get('user-google')
const token = c.get('token')
const grantedScopes = c.get('granted-scopes')
return c.json({
user,
token,
grantedScopes,
})
})
2024-01-08 04:22:20 +08:00
// Facebook
app.use(
'/facebook',
facebookAuth({
client_id,
client_secret,
scope: ['email', 'public_profile'],
fields: [
'email',
'id',
'first_name',
'last_name',
'middle_name',
'name',
'picture',
'short_name',
],
})
)
app.use('/facebook-custom-redirect', (c, next) =>
facebookAuth({
client_id,
client_secret,
scope: [],
fields: [],
redirect_uri: 'http://localhost:3000/facebook',
})(c, next)
)
app.get('/facebook', (c) => {
const user = c.get('user-facebook')
const token = c.get('token')
const grantedScopes = c.get('granted-scopes')
return c.json({
user,
token,
grantedScopes,
})
})
2024-01-08 04:22:20 +08:00
// Github
app.use(
'/github/app',
githubAuth({
client_id,
client_secret,
})
)
app.use('/github/app-custom-redirect', (c, next) =>
githubAuth({
client_id,
client_secret,
redirect_uri: 'http://localhost:3000/github/app',
})(c, next)
)
app.get('/github/app', (c) => {
const token = c.get('token')
const refreshToken = c.get('refresh-token')
const user = c.get('user-github')
const grantedScopes = c.get('granted-scopes')
return c.json({
token,
refreshToken,
user,
grantedScopes,
})
})
app.use(
'/github/oauth-app',
githubAuth({
client_id,
client_secret,
scope: ['public_repo', 'read:user', 'user', 'user:email', 'user:follow'],
oauthApp: true,
})
)
app.get('/github/oauth-app', (c) => {
const token = c.get('token')
const user = c.get('user-github')
const grantedScopes = c.get('granted-scopes')
return c.json({
user,
token,
grantedScopes,
})
})
2024-01-08 04:22:20 +08:00
// LinkedIn
app.use(
'/linkedin',
linkedinAuth({
client_id,
client_secret,
scope: ['email', 'openid', 'profile'],
})
)
app.use('/linkedin-custom-redirect', (c, next) =>
linkedinAuth({
client_id,
client_secret,
scope: ['email', 'openid', 'profile'],
redirect_uri: 'http://localhost:3000/linkedin',
})(c, next)
)
app.get('/linkedin', (c) => {
const token = c.get('token')
const refreshToken = c.get('refresh-token')
const user = c.get('user-linkedin')
const grantedScopes = c.get('granted-scopes')
return c.json({
token,
refreshToken,
grantedScopes,
user,
})
})
2024-01-08 04:22:20 +08:00
// X
app.use(
'/x',
xAuth({
client_id,
client_secret,
scope: ['tweet.read', 'users.read', 'follows.read', 'follows.write', 'offline.access'],
fields: [
'created_at',
'description',
'entities',
'location',
'most_recent_tweet_id',
'pinned_tweet_id',
'profile_image_url',
'protected',
'public_metrics',
'url',
'verified',
'verified_type',
'withheld',
],
})
)
app.use('/x-custom-redirect', (c, next) =>
xAuth({
client_id,
client_secret,
scope: [],
fields: [],
redirect_uri: 'http://localhost:3000/x',
})(c, next)
)
app.get('/x', (c) => {
const token = c.get('token')
const refreshToken = c.get('refresh-token')
const user = c.get('user-x')
const grantedScopes = c.get('granted-scopes')
return c.json({
token,
refreshToken,
grantedScopes,
user,
})
})
app.get('x/refresh', async (c) => {
const response = await refreshToken(
client_id,
client_secret,
'MzJvY0QyNmNzWUtBU3BUelpOU1NLdXFOd05qdGROZFhtR3o3QkpPNHZpQ2xrOjE3MDEyOTU0ODkxMzM6MTowOnJ0OjE'
)
return c.json(response)
})
app.get('x/refresh/error', async (c) => {
const response = await refreshToken(client_id, client_secret, 'wrong-refresh-token')
return c.json(response)
})
app.get('/x/revoke', async (c) => {
const response = await revokeToken(
client_id,
client_secret,
'RkNwZzE4X0EtRmNkWTktN1hoYmdWSFQ4RjBPTzhvNGZod01lZmIxSjY0Xy1pOjE3MDEyOTYyMTY1NjM6MToxOmF0OjE'
)
return c.json(response)
})
app.get('x/revoke/error', async (c) => {
const response = await revokeToken(client_id, client_secret, 'wrong-token')
return c.json(response)
})
2024-01-08 04:22:20 +08:00
// Discord
app.use(
'/discord',
discordAuth({
client_id,
client_secret,
scope: ['identify', 'email'],
})
)
app.use('/discord-custom-redirect', (c, next) =>
discordAuth({
client_id,
client_secret,
scope: ['identify', 'email'],
redirect_uri: 'http://localhost:3000/discord',
})(c, next)
)
2024-01-08 04:22:20 +08:00
app.get('/discord', (c) => {
const token = c.get('token')
const refreshToken = c.get('refresh-token')
const user = c.get('user-discord')
const grantedScopes = c.get('granted-scopes')
return c.json({
token,
refreshToken,
grantedScopes,
user,
})
})
app.get('/discord/refresh', async (c) => {
const response = await discordRefresh(
client_id,
client_secret,
'MzJvY0QyNmNzWUtBU3BUelpOU1NLdXFOd05qdGROZFhtR3o3QkpPNHZpQ2xrOjE3MDEyOTU0ODkxMzM6MTowOnJ0OjE'
)
return c.json(response)
})
app.get('/discord/refresh/error', async (c) => {
const response = await discordRefresh(client_id, client_secret, 'wrong-refresh-token')
return c.json(response)
})
app.get('/discord/revoke', async (c) => {
const response = await discordRevoke(
client_id,
client_secret,
'RkNwZzE4X0EtRmNkWTktN1hoYmdWSFQ4RjBPTzhvNGZod01lZmIxSjY0Xy1pOjE3MDEyOTYyMTY1NjM6MToxOmF0OjE'
)
return c.json(response)
})
app.get('/discord/revoke/error', async (c) => {
const response = await discordRevoke(client_id, client_secret, 'wrong-token')
return c.json(response)
})
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
describe('googleAuth middleware', () => {
it('Should redirect', async () => {
const res = await app.request('/google')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
expect(res.headers)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/google-custom-redirect')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/google')
})
it('Should attach custom parameters', async () => {
const res = await app.request('/google-custom-params')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/google')
expect(redirectUrl.searchParams.get('scope')).toBe('openid email profile')
expect(redirectUrl.searchParams.get('login_hint')).toBe('test-login-hint')
expect(redirectUrl.searchParams.get('prompt')).toBe('select_account')
expect(redirectUrl.searchParams.get('state')).toBe('test-state')
expect(redirectUrl.searchParams.get('access_type')).toBe('offline')
})
it('Prevent CSRF attack', async () => {
const res = await app.request(`/google?code=${dummyCode}&state=malware-state`)
expect(res).not.toBeNull()
expect(res.status).toBe(401)
})
it('Should throw error for invalide code', async () => {
const res = await app.request('/google?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(googleCodeError.error.message)
})
it('Should work with received code', async () => {
const res = await app.request(`/google?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
user: GoogleUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(googleUser)
expect(response.grantedScopes).toEqual(dummyToken.scope.split(' '))
expect(response.token).toEqual({
token: dummyToken.access_token,
expires_in: dummyToken.expires_in,
})
})
})
describe('facebookAuth middleware', () => {
it('Should redirect', async () => {
const res = await app.request('/facebook')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/facebook-custom-redirect')
expect(res?.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/facebook')
})
it('Prevent CSRF attack', async () => {
const res = await app.request(`/facebook?code=${dummyCode}&state=malware-state`)
expect(res).not.toBeNull()
expect(res.status).toBe(401)
})
it('Should throw error for invalid code', async () => {
const res = await app.request('/facebook?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(facebookCodeError.error.message)
})
it('Should work with received code', async () => {
const res = await app.request(
`/facebook?code=${dummyCode}&granted_scopes=email%2Cpublic_profile`
)
const response = (await res.json()) as {
token: Token
user: FacebookUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(facebookUser)
expect(response.grantedScopes).toEqual(['email', 'public_profile'])
expect(response.token).toEqual({
token: dummyToken.access_token,
expires_in: dummyToken.expires_in,
})
})
})
describe('githubAuth middleware', () => {
describe('Github with Github App', () => {
it('Should redirect', async () => {
const res = await app.request('/github/app')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/github/app-custom-redirect')
expect(res?.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe(
'http://localhost:3000/github/app'
)
})
it('Should throw error for invalide code', async () => {
const res = await app.request('/github/app?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(githubCodeError.error_description)
})
it('Should work with received code', async () => {
const res = await app.request(`/github/app?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
refreshToken: Token
user: GitHubUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(githubUser)
expect(response.grantedScopes).toEqual(['public_repo', 'user'])
expect(response.token).toEqual({
token: githubToken.access_token,
expires_in: githubToken.expires_in,
})
expect(response.refreshToken).toEqual({
token: githubToken.refresh_token,
expires_in: githubToken.refresh_token_expires_in,
})
})
})
describe('Github with OAuth App', () => {
it('Should redirect', async () => {
const res = await app.request('/github/oauth-app')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
})
it('Should throw error for invalide code', async () => {
const res = await app.request('/github/oauth-app?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(githubCodeError.error_description)
})
it('Should work with received code', async () => {
const res = await app.request(`/github/oauth-app?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
user: GitHubUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(githubUser)
expect(response.grantedScopes).toEqual(['public_repo', 'user'])
expect(response.token).toEqual({
token: githubToken.access_token,
expires_in: githubToken.expires_in,
})
})
})
})
describe('linkedinAuth middleware', () => {
it('Should redirect', async () => {
const res = await app.request('/linkedin')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/linkedin-custom-redirect')
expect(res?.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/linkedin')
})
it('Should throw error for invalide code', async () => {
const res = await app.request('/linkedin?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(linkedInCodeError.error)
})
it('Should work with received code', async () => {
const res = await app.request(`/linkedin?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
refreshToken: Token
user: LinkedInUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(linkedInUser)
expect(response.grantedScopes).toEqual(['email', 'openid', 'profile'])
expect(response.token).toEqual({
token: linkedInToken.access_token,
expires_in: linkedInToken.expires_in,
})
expect(response.refreshToken).toEqual({
token: linkedInToken.refresh_token,
expires_in: linkedInToken.refresh_token_expires_in,
})
})
})
describe('xAuth middleware', () => {
describe('middleware', () => {
it('Should redirect', async () => {
const res = await app.request('/x')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/x-custom-redirect')
expect(res?.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/x')
})
it('Prevent CSRF attack', async () => {
const res = await app.request(`/x?code=${dummyCode}&state=malware-state`)
expect(res).not.toBeNull()
expect(res.status).toBe(401)
})
it('Should throw error for invalid code', async () => {
const res = await app.request('/x?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(xCodeError.error_description)
})
it('Should work with received code', async () => {
const res = await app.request(`/x?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
refreshToken: Token
user: XUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(xUser.data)
expect(response.grantedScopes).toEqual([
'tweet.read',
'users.read',
'follows.read',
'follows.write',
'offline.access',
])
expect(response.token).toEqual({
token: xToken.access_token,
expires_in: xToken.expires_in,
})
expect(response.refreshToken).toEqual({
token: xToken.refresh_token,
expires_in: 0,
})
})
})
describe('Refresh Token', () => {
it('Should refresh token', async () => {
const res = await app.request('/x/refresh')
expect(res).not.toBeNull()
expect(await res.json()).toEqual(xRefreshToken)
})
it('Should return error for refresh', async () => {
const res = await app.request('/x/refresh/error')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(xRefreshTokenError.error_description)
})
})
describe('Revoke Token', () => {
it('Should revoke token', async () => {
const res = await app.request('/x/revoke')
expect(res).not.toBeNull()
expect(await res.json()).toEqual(true)
})
it('Should return error for revoke', async () => {
const res = await app.request('/x/revoke/error')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(xRevokeTokenError.error_description)
})
})
})
2024-01-08 04:22:20 +08:00
describe('discordAuth middleware', () => {
describe('middleware', () => {
it('Should redirect', async () => {
const res = await app.request('/discord')
expect(res).not.toBeNull()
expect(res.status).toBe(302)
})
it('Should redirect to custom redirect_uri', async () => {
const res = await app.request('/discord-custom-redirect')
expect(res?.status).toBe(302)
const redirectLocation = res.headers.get('location')!
const redirectUrl = new URL(redirectLocation)
expect(redirectUrl.searchParams.get('redirect_uri')).toBe('http://localhost:3000/discord')
})
2024-01-08 04:22:20 +08:00
it('Prevent CSRF attack', async () => {
const res = await app.request(`/discord?code=${dummyCode}&state=malware-state`)
expect(res).not.toBeNull()
expect(res.status).toBe(401)
})
it('Should throw error for invalid code', async () => {
const res = await app.request('/discord?code=9348ffdsd-sdsdbad-code')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(discordCodeError.error)
})
it('Should work with received code', async () => {
const res = await app.request(`/discord?code=${dummyCode}`)
const response = (await res.json()) as {
token: Token
refreshToken: Token
user: DiscordUser
grantedScopes: string[]
}
expect(res).not.toBeNull()
expect(res.status).toBe(200)
expect(response.user).toEqual(discordUser.user)
expect(response.grantedScopes).toEqual(['identify', 'email'])
expect(response.token).toEqual({
token: discordToken.access_token,
expires_in: discordToken.expires_in,
})
expect(response.refreshToken).toEqual({
token: discordToken.refresh_token,
expires_in: 0,
})
})
})
describe('Refresh Token', () => {
it('Should refresh token', async () => {
const res = await app.request('/discord/refresh')
expect(res).not.toBeNull()
expect(await res.json()).toEqual(discordRefreshToken)
})
it('Should return error for refresh', async () => {
const res = await app.request('/discord/refresh/error')
expect(res).not.toBeNull()
expect(res.status).toBe(400)
expect(await res.text()).toBe(discordRefreshTokenError.error)
})
})
})
})