Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
85537e2
feat(cloudflare-access): add optional audience (aud) validation to pr…
gradyat Mar 17, 2026
5fada61
fix(cloudflare-access): fix inverted JWKS cache invalidation logic
gradyat Mar 17, 2026
dfbce94
fix(cloudflare-access): validate accessTeamName format
gradyat Mar 17, 2026
b6dfae8
fix(cloudflare-access): warn when aud parameter is omitted
gradyat Mar 17, 2026
ba0a75f
refactor(cloudflare-access): add proper type for JWT header
gradyat Mar 17, 2026
029b545
fix(cloudflare-access): use proper base64url decoding for all JWT seg…
gradyat Mar 17, 2026
0ec7023
fix(cloudflare-access): validate JWT alg header is RS256
gradyat Mar 17, 2026
0c1fcc5
fix(cloudflare-access): add runtime validation for decoded JWT payload
gradyat Mar 17, 2026
6c57ad1
fix(cloudflare-access): validate nbf (not before) claim
gradyat Mar 17, 2026
bb2ed5c
fix(cloudflare-access): prevent information leakage in issuer error
gradyat Mar 17, 2026
24b9e44
fix(cloudflare-access): normalize error message casing
gradyat Mar 17, 2026
40d7725
fix(cloudflare-access): fix test description typos and imports
gradyat Mar 17, 2026
b6edcaa
test(cloudflare-access): add tests for new security validations
gradyat Mar 17, 2026
292e8ad
docs(cloudflare-access): update error table with new validation errors
gradyat Mar 17, 2026
d18bbcf
style(cloudflare-access): apply prettier formatting
gradyat Mar 17, 2026
3425d91
fix(cloudflare-access): make nbf optional in CloudflareAccessPayload …
gradyat Mar 17, 2026
ca5b3ce
fix(cloudflare-access): reorder JWT validation for security and perfo…
gradyat Mar 17, 2026
d33c420
test(cloudflare-access): clean up test helpers and improve test isola…
gradyat Mar 17, 2026
34b360a
test(cloudflare-access): add proper cache expiration test with fake t…
gradyat Mar 17, 2026
e7f9902
fix(cloudflare-access): harden JWT parsing and JWKS key filtering
gradyat Mar 17, 2026
a875c9c
fix(cloudflare-access): add key_ops filtering and missing kid guard p…
gradyat Mar 17, 2026
542120f
fix(cloudflare-access): make CloudflareAccessPayload fields accuratel…
gradyat Mar 17, 2026
f39e9c5
fix(cloudflare-access): harden aud validation against substring matching
gradyat Mar 17, 2026
e1850ea
fix(cloudflare-access): validate base64url input per RFC 7515 §5.2
gradyat Mar 17, 2026
e944ce0
fix(cloudflare-access): add 30-second clock skew tolerance for exp an…
gradyat Mar 17, 2026
dce8c91
fix(cloudflare-access): re-fetch JWKS on kid cache miss for key rotation
gradyat Mar 17, 2026
204724b
feat(cloudflare-access): accept multiple audiences in aud parameter
gradyat Mar 17, 2026
d20292d
style(cloudflare-access): run prettier
gradyat Mar 17, 2026
78eed25
docs(cloudflare-access): add missing "Unsupported critical extension"…
gradyat Mar 17, 2026
25e891c
test(cloudflare-access): fix TypeScript type error in test payload as…
gradyat Mar 17, 2026
eed6d99
fix(cloudflare-access): remove JWKS re-fetch rate limiter
gradyat Mar 18, 2026
3ab235a
style(cloudflare-access): Revert nit casing
gradyat Mar 18, 2026
b934ae9
style(cloudflare-access): Revert comment deletion
gradyat Mar 18, 2026
14b0841
fix(cloudflare-access): decode base64url payload as UTF-8
gradyat Mar 18, 2026
19090e5
style(cloudflare-access): Run prettier
gradyat Mar 18, 2026
cbe6ffc
chore(cloudflare-access): add changeset for minor release
gradyat Mar 18, 2026
633bbf3
fix(cloudflare-access): use binary decode for JWT signature
gradyat Mar 20, 2026
74035e1
docs(cloudflare-access): handle optional email in README example
gradyat Mar 20, 2026
051a206
fix(cloudflare-access): only re-fetch JWKS after signature failure
gradyat Mar 20, 2026
1f05ae0
fix(cloudflare-access): correct RFC citations in comments
gradyat Mar 20, 2026
b946ba3
test(cloudflare-access): add clock skew boundary rejection tests
gradyat Mar 20, 2026
6061d85
fix(cloudflare-access): harden team name, exp boundary, kid fallback,…
gradyat Mar 20, 2026
cc05d3a
fix(cloudflare-access): resolve TypeScript type errors
gradyat Mar 20, 2026
5ba880c
test(cloudflare-access): add coverage for base64urlDecodeToBytes inva…
gradyat Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/small-socks-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/cloudflare-access': minor
---

Add audience (aud) validation and harden JWT verification
42 changes: 30 additions & 12 deletions packages/cloudflare-access/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This middleware can be used to validate that your application is being served be
JWT received, User details from the JWT are also available inside the request context.

This middleware will also ensure the Access policy serving the application is from a
specific [Access Team](https://developers.cloudflare.com/cloudflare-one/faq/getting-started-faq/#whats-a-team-domainteam-name).
specific [Access Team](https://developers.cloudflare.com/cloudflare-one/faq/getting-started-faq/#whats-a-team-domainteam-name). It is strongly recommended to pass your [Application Audience (AUD) Tag](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#get-your-aud-tag) to validate that the JWT was intended for your specific application to prevent cross-application token reuse.

## Usage

Expand All @@ -19,12 +19,24 @@ import { Hono } from 'hono'

const app = new Hono()

app.use('*', cloudflareAccess('my-access-team-name'))
app.use('*', cloudflareAccess('my-access-team-name', 'my-application-aud-tag'))
app.get('/', (c) => c.text('foo'))

export default app
```

The `aud` parameter accepts a single string or an array of strings for workers serving multiple Access applications:

```ts
// Single audience
app.use('*', cloudflareAccess('my-team', 'aud-tag-1'))

// Multiple audiences (e.g. production and preview applications)
app.use('*', cloudflareAccess('my-team', ['aud-tag-1', 'aud-tag-2']))
```

The `aud` parameter is optional for backwards compatibility, but omitting it is discouraged in production.

## Access JWT payload

```ts
Expand All @@ -41,23 +53,29 @@ 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}`)
return c.text(payload.email ? `Hello, ${payload.email}` : 'Hello, authenticated user')
})

export default app
```

## Errors throw by the middleware

| Error | HTTP Code |
| ------------------------------------------------------------------------------------------------------ | --------- |
| Authentication error: Missing bearer token | 401 |
| Authentication error: Unable to decode Bearer token | 401 |
| 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 |
| Error | HTTP Code |
| ------------------------------------------------------------------------------ | --------- |
| Authentication error: Missing bearer token | 401 |
| Authentication error: Unable to decode Bearer token | 401 |
| Authentication error: Invalid token algorithm | 401 |
| Authentication error: Unsupported critical extension | 401 |
| Authentication error: Malformed token payload | 401 |
| Authentication error: Token is expired | 401 |
| Authentication error: Token is not yet valid | 401 |
| Authentication error: Invalid Token | 401 |
| Authentication error: Invalid team name | 401 |
| Authentication error: Invalid token audience | 401 |
| Invalid accessTeamName: must contain only alphanumeric characters and hyphens | (throws) |
| Authentication error: The Access Organization 'my-team-name' does not exist | 500 |
| Authentication error: Received unexpected HTTP code 500 from Cloudflare Access | 500 |

## Author

Expand Down
Loading
Loading