index.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'
  2. console.log('main function started')
  3. const JWT_SECRET = Deno.env.get('JWT_SECRET')
  4. const BRIVEN_URL = Deno.env.get('BRIVEN_URL')
  5. const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
  6. // Create JWKS for ES256/RS256 tokens (newer tokens)
  7. let BRIVEN_JWT_KEYS: ReturnType<typeof jose.createRemoteJWKSet> | null = null
  8. if (BRIVEN_URL) {
  9. try {
  10. BRIVEN_JWT_KEYS = jose.createRemoteJWKSet(
  11. new URL('/auth/v1/.well-known/jwks.json', BRIVEN_URL)
  12. )
  13. } catch (e) {
  14. console.error('Failed to fetch JWKS from BRIVEN_URL:', e)
  15. }
  16. }
  17. /**
  18. * Extract JWT token from Authorization header
  19. *
  20. * Parses the Authorization header to extract the Bearer token.
  21. * Expects format: "Bearer <token>"
  22. *
  23. * @param req - The HTTP request object
  24. * @returns The JWT token string
  25. * @throws Error if Authorization header is missing or malformed
  26. */
  27. function getAuthToken(req: Request) {
  28. const authHeader = req.headers.get('authorization')
  29. if (!authHeader) {
  30. throw new Error('Missing authorization header')
  31. }
  32. const [bearer, token] = authHeader.split(' ')
  33. if (bearer !== 'Bearer') {
  34. throw new Error(`Auth header is not 'Bearer {token}'`)
  35. }
  36. return token
  37. }
  38. async function isValidLegacyJWT(jwt: string): Promise<boolean> {
  39. if (!JWT_SECRET) {
  40. console.error('JWT_SECRET not available for HS256 token verification')
  41. return false
  42. }
  43. const encoder = new TextEncoder();
  44. const secretKey = encoder.encode(JWT_SECRET)
  45. try {
  46. await jose.jwtVerify(jwt, secretKey);
  47. } catch (e) {
  48. console.error('Symmetric Legacy JWT verification error', e);
  49. return false;
  50. }
  51. return true;
  52. }
  53. async function isValidJWT(jwt: string): Promise<boolean> {
  54. if (!BRIVEN_JWT_KEYS) {
  55. console.error('JWKS not available for ES256/RS256 token verification')
  56. return false
  57. }
  58. try {
  59. await jose.jwtVerify(jwt, BRIVEN_JWT_KEYS)
  60. } catch (e) {
  61. console.error('Asymmetric JWT verification error', e);
  62. return false
  63. }
  64. return true;
  65. }
  66. /**
  67. * Verify JWT token, handling both legacy (HS256) and newer (ES256/RS256) algorithms
  68. *
  69. * This function automatically detects the algorithm used in the token and applies
  70. * the appropriate verification method:
  71. * - HS256: Uses JWT_SECRET (symmetric key)
  72. * - ES256/RS256: Uses JWKS endpoint (asymmetric public keys)
  73. *
  74. * This fix ensures compatibility with both legacy tokens and newer asymmetric tokens,
  75. * resolving the "Key for the ES256 algorithm must be of type CryptoKey" error.
  76. *
  77. * @param jwt - The JWT token string to verify
  78. * @returns Promise resolving to true if verification succeeds, false otherwise
  79. */
  80. async function isValidHybridJWT(jwt: string): Promise<boolean> {
  81. const { alg: jwtAlgorithm } = jose.decodeProtectedHeader(jwt)
  82. if (jwtAlgorithm === 'HS256') {
  83. console.log(`Legacy token type detected, attempting ${jwtAlgorithm} verification.`)
  84. return await isValidLegacyJWT(jwt)
  85. }
  86. if (jwtAlgorithm === 'ES256' || jwtAlgorithm === 'RS256') {
  87. return await isValidJWT(jwt)
  88. }
  89. return false;
  90. }
  91. Deno.serve(async (req: Request) => {
  92. if (req.method !== 'OPTIONS' && VERIFY_JWT) {
  93. try {
  94. const token = getAuthToken(req)
  95. const isValidJWT = await isValidHybridJWT(token);
  96. if (!isValidJWT) {
  97. return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
  98. status: 401,
  99. headers: { 'Content-Type': 'application/json' },
  100. })
  101. }
  102. } catch (e) {
  103. console.error(e)
  104. return new Response(JSON.stringify({ msg: e.toString() }), {
  105. status: 401,
  106. headers: { 'Content-Type': 'application/json' },
  107. })
  108. }
  109. }
  110. const url = new URL(req.url)
  111. const { pathname } = url
  112. const path_parts = pathname.split('/')
  113. const service_name = path_parts[1]
  114. if (!service_name || service_name === '') {
  115. const error = { msg: 'missing function name in request' }
  116. return new Response(JSON.stringify(error), {
  117. status: 400,
  118. headers: { 'Content-Type': 'application/json' },
  119. })
  120. }
  121. const servicePath = `/home/deno/functions/${service_name}`
  122. console.error(`serving the request with ${servicePath}`)
  123. const memoryLimitMb = 150
  124. const workerTimeoutMs = 1 * 60 * 1000
  125. const noModuleCache = false
  126. const importMapPath = null
  127. const envVarsObj = Deno.env.toObject()
  128. const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
  129. try {
  130. const worker = await EdgeRuntime.userWorkers.create({
  131. servicePath,
  132. memoryLimitMb,
  133. workerTimeoutMs,
  134. noModuleCache,
  135. importMapPath,
  136. envVars,
  137. })
  138. return await worker.fetch(req)
  139. } catch (e) {
  140. const error = { msg: e.toString() }
  141. return new Response(JSON.stringify(error), {
  142. status: 500,
  143. headers: { 'Content-Type': 'application/json' },
  144. })
  145. }
  146. })