feat(cloudflare-access): Handle Access organization does not exist and Access not available cases (#898)

pull/902/head
Gabriel Massadas 2024-12-23 02:19:56 +00:00 committed by GitHub
parent cd6c667ee2
commit b71d817f71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 9 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/cloudflare-access': minor
---
Handle Access organization does not exist and Access not available cases

View File

@ -38,14 +38,13 @@ const app = new Hono<{ Variables: myVariables & CloudflareAccessVariables }>()
app.use('*', cloudflareAccess('my-access-team-name'))
app.get('/', (c) => {
const payload = c.get('accessPayload')
return c.text(`You just authenticated with the email ${payload.email}`)
})
export default app
```
## Errors throw by the middleware
| Error | HTTP Code |
@ -55,6 +54,8 @@ export default app
| Authentication error: Token is expired | 401 |
| Authentication error: Expected team name {your-team-name}, but received ${different-team-signed-token} | 401 |
| Authentication error: Invalid Token | 401 |
| Authentication error: The Access Organization 'my-team-name' does not exist | 500 |
| Authentication error: Received unexpected HTTP code 500 from Cloudflare Access | 500 |
## Author

View File

@ -116,14 +116,17 @@ describe('Cloudflare Access middleware', async () => {
const keyPair2 = await generateJWTKeyPair();
const keyPair3 = await generateJWTKeyPair();
vi.stubGlobal('fetch', async () => {
return Response.json({
keys: [
publicKeyToJWK(keyPair1.publicKey),
publicKeyToJWK(keyPair2.publicKey),
],
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal('fetch', async () => {
return Response.json({
keys: [
publicKeyToJWK(keyPair1.publicKey),
publicKeyToJWK(keyPair2.publicKey),
],
})
})
})
});
const app = new Hono()
@ -131,6 +134,12 @@ describe('Cloudflare Access middleware', async () => {
app.get('/hello-behind-access', (c) => c.text('foo'))
app.get('/access-payload', (c) => c.json(c.get('accessPayload')))
app.onError((err, c) => {
return c.json({
err: err.toString(),
}, 500)
})
it('Should be throw Missing bearer token when nothing is sent', async () => {
const res = await app.request('http://localhost/hello-behind-access')
expect(res).not.toBeNull()
@ -248,4 +257,34 @@ describe('Cloudflare Access middleware', async () => {
"exp":expect.any(Number)
})
})
it('Should throw an error, if the access organization does not exist', async () => {
vi.stubGlobal('fetch', async () => {
return Response.json({success: false}, {status: 404})
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': 'asdads'
}
})
expect(res).not.toBeNull()
expect(res.status).toBe(500)
expect(await res.json()).toEqual({"err":"Error: Authentication error: The Access Organization 'my-cool-team-name' does not exist"})
})
it('Should throw an error, if the access certs url is unavailable', async () => {
vi.stubGlobal('fetch', async () => {
return Response.json({success: false}, {status: 500})
})
const res = await app.request('http://localhost/hello-behind-access', {
headers: {
'cf-access-jwt-assertion': 'asdads'
}
})
expect(res).not.toBeNull()
expect(res.status).toBe(500)
expect(await res.json()).toEqual({"err":"Error: Authentication error: Received unexpected HTTP code 500 from Cloudflare Access"})
})
})

View File

@ -1,5 +1,6 @@
import { createMiddleware } from 'hono/factory'
import { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export type CloudflareAccessPayload = {
aud: string[],
@ -89,6 +90,14 @@ async function getPublicKeys(accessTeamName: string) {
},
})
if (!result.ok) {
if (result.status === 404) {
throw new HTTPException(500, { message: `Authentication error: The Access Organization '${accessTeamName}' does not exist` })
}
throw new HTTPException(500, { message: `Authentication error: Received unexpected HTTP code ${result.status} from Cloudflare Access` })
}
const data: any = await result.json()
// Because we keep CryptoKey's in memory between requests, we need to make sure they are refreshed once in a while