-
-
Notifications
You must be signed in to change notification settings - Fork 662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for asymmetric JWT validation (e.g. Auth0 or Clerk) #672
Comments
Hi @cd-slash ! Thank you for your proposal. It looks good. It's OK to go with it, but we have things to consider.
If you want to make this as third-party middleware, you can develop it in the monorepo github.com/honojs/middleware. And we will also distribute it under Hi @metrue : |
Thanks for your comments - I believe I can rewrite to use browser APIs like |
I wanted to use |
J/W - did you open an issue with panva? He seems very responsive. If you were testing on miniflare prior to September, you may have run into this. |
I found some time to work on this and put together a quick proof of concept to demonstrate that this works without external dependencies, as below: import type { MiddlewareHandler, Context } from "hono"
// modified from swansontec/rfc4648.js
const parseBase64Url = (string: string): Uint8Array => {
const encodingChars: string =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
const encodingBits: number = 6
// reduce encoding chars to codes using reduce
const encodingCodes: { [key: string]: number } = {}
encodingChars.split("").reduce((acc, char, index) => {
acc[char] = index
return acc
}, encodingCodes)
// Count the padding bytes:
let end = string.length
while (string[end - 1] === "=") {
--end
}
const output = new Uint8Array(((end * encodingBits) / 8) | 0)
// Parse the data:
let bits = 0 // Number of bits currently in the buffer
let buffer = 0 // Bits waiting to be written out, MSB first
let written = 0 // Next byte to write
for (let i = 0; i < end; ++i) {
// Read one character from the string:
const value = encodingCodes[string[i]]
if (value === undefined) {
throw new SyntaxError("Invalid character " + string[i])
}
// Append the bits to the buffer:
buffer = (buffer << encodingBits) | value
bits += encodingBits
// Write out some bits if the buffer has a byte's worth:
if (bits >= 8) {
bits -= 8
output[written++] = 0xff & (buffer >> bits)
}
}
// Verify that we have received just enough bits:
if (bits >= encodingBits || 0xff & (buffer << (8 - bits))) {
throw new SyntaxError("Unexpected end of data")
}
return output
}
const parseJwtPayload = <T extends object = { [k: string]: string | number }>(
token: string
): T | undefined => {
// convert token from base64url to base64
token = token.replace(/-/g, "+").replace(/_/g, "/")
try {
return JSON.parse(atob(token.split(".")[1]))
} catch {
return undefined
}
}
const verify = async (jwsObject: string, jwKey: JsonWebKey, c: Context) => {
const jwsSigningInput = jwsObject.split(".").slice(0, 2).join(".")
const jwsSignature = jwsObject.split(".")[2]
const key = await crypto.subtle.importKey(
"jwk",
jwKey,
{
name: "RSASSA-PKCS1-v1_5",
hash: { name: "SHA-256" },
},
false,
["verify"]
)
return await crypto.subtle.verify(
{ name: "RSASSA-PKCS1-v1_5" },
key,
parseBase64Url(jwsSignature),
new TextEncoder().encode(jwsSigningInput)
)
}
const getPublicKeys = async (
c: Context
): Promise<{ keys: JsonWebKey[] } | null> => {
const cache: KVNamespace = c.env.PUBLIC_JWK_CACHE
const cachedKeys: string | null = await cache.get("public-keys")
if (cachedKeys != null) {
return JSON.parse(cachedKeys)
} else {
try {
const res = await fetch(`${c.env.AUTH_DOMAIN}/.well-known/jwks.json`)
const freshKeys: { keys: JsonWebKey[] } = await res.json()
try {
await cache.put("public-keys", JSON.stringify(freshKeys), {
expirationTtl: 432000,
})
} catch (e) {
// don't fail as the validation can still occur if keys can't be cached
console.log("Error caching public keys: ", e)
}
return freshKeys
} catch (e) {
console.log("Error getting fresh keys: ", e)
return null
}
}
}
export const asymmetricJwt = (): MiddlewareHandler => {
return async (c, next) => {
let jwt: string
const authorization = c.req.header("Authorization")
if (authorization == null) {
return c.json({ error: "No auth token found" }, 401)
} else {
jwt = authorization.replace(/Bearer\s+/i, "")
}
// get public keys from cache, or get new keys and cache them (5 day TTL)
const publicKeys = await getPublicKeys(c)
if (publicKeys == null) {
return c.json({ error: "Failed to fetch public keys" }, 500)
}
const validationResponses = await Promise.all(
publicKeys.keys.map(async (key) => {
return await verify(jwt, key, c)
})
)
if (!validationResponses.includes(true)) {
return c.json({ error: "Invalid token" }, 401)
} else {
const payload = parseJwtPayload(jwt)
if (payload == null) {
return c.json({ error: "Invalid token payload" }, 401)
}
// validate claims
if (payload.exp < Date.now() / 1000) {
return c.json({ error: "Token expired" }, 401)
} else {
console.log(
`Token expires in ${Math.floor(
(payload.exp as number) - Date.now() / 1000
)} seconds`
)
}
if (payload.aud !== c.env.AUTH_AUDIENCE) {
return c.json({ error: "Invalid audience" }, 401)
}
if (payload.iss !== c.env.AUTH_DOMAIN) {
return c.json({ error: "Invalid issuer" }, 401)
}
c.set("claims", payload)
c.set("user_id", payload.id)
c.set("org_id", payload.org_id)
c.set("org_role", payload.org_role)
await next()
}
}
} I need to do some work to tidy this up (e.g. moving the claims validation to options) but need to do some research on the various auth providers to make it more durable. Thoughts welcome on the general direction here. |
@AlexErrant Sorry for the late reply. Indeed, it is looks like an issue with Miniflare rather than jose. |
Hey @cd-slash, thank you for raising this issue. Any updates by change on your draft? I'm evaluating using Clerk in an app and it would've been great to have a built-in or third-party Hono middleware to use for auth with Clerk. @yusukebe do you have any recommendations on how to use Clerk with Hono from Cloudflare Workers? Thanks! |
Hello, @cd-slash @uicrafts - I created this project to address the issue. The project includes a Hono Middleware. Can you test it when you have a chance? |
Hello. I think a good approach would be to add support for handling asymmetry token to the existing JWT Auth middleware. What do you think? |
I am using the middleware introduced in #3826. It validates the token but marks it as invalid beause the JWT header contains Lines 37 to 47 in 9892b47
Monkey-patching this check to I don't know if changing that check that would be a breaking yet insecure change. The auth0 docs state that this header is used in So in case someone encounters this issue, too, try setting |
Are there any examples of this, or something similar, working with Auth0? Similar to how they recommend in the below example?
|
@searledan It's as simple as the following: import { Hono } from "hono";
import { jwk } from "hono/jwk";
const app = new Hono();
app.use(
"/api/*",
jwk({
jwks_uri: import.meta.env.VITE_AUTH0_WELL_KNOWN_JWKS_URL,
}),
);
app.get("/api/protected", async (c) => {
const jwtPayload = c.get("jwtPayload");
return c.json({
...(jwtPayload as Record<string, unknown>),
"i-am-protected": "Whoohoo!",
});
});
export default app; The |
The existing JWT middleware validates tokens where the secret is available (i.e. symmetric validation) but services like Auth0, Clerk and others use asymmetric tokens signed by the service using a secret key that is not made available for validating the token. These tokens need to be validated with the public key.
Should validating asymmetric tokens be supported in Hono? It seems like it would be very useful for anyone who wants to build an API accessible by end users.
I put together a quick proof of concept middleware for Clerk tokens, which grabs the public key(s), caches them in a Cloudflare workers KV and uses them to validate the token. The token's claims are made available to the context using the
c.set()
function. This code could be refined if it's something that should be supported, and note that the code as drafted introduces a dependency on the jose library for key import and validation.The text was updated successfully, but these errors were encountered: