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() // 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, }) }) // 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, }) }) // 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, }) }) // 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, }) }) // 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) }) // 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) ) 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) }) }) }) 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') }) 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) }) }) }) })