Skip to content

feat: add hybrid jwt verification#4721

Open
kallebysantos wants to merge 3 commits intosupabase:developfrom
kallebysantos:fix-update-functions-verify-jwt
Open

feat: add hybrid jwt verification#4721
kallebysantos wants to merge 3 commits intosupabase:developfrom
kallebysantos:fix-update-functions-verify-jwt

Conversation

@kallebysantos
Copy link
Member

@kallebysantos kallebysantos commented Jan 14, 2026

What kind of change does this PR introduce?

Bug fix, feature

What is the current behavior?

Since API keys did change, users can't call functions without manually disabling verify_jwt.

What is the new behavior?

JWKs are now default exposed #4688, so It allows to verify both new asymmetric tokens as well legacy ones.

This way it applies a temporary fix while migrating API keys.

Allows verify new JWTs as well legacy
@kallebysantos kallebysantos requested a review from a team as a code owner January 14, 2026 11:38
@coveralls
Copy link

Pull Request Test Coverage Report for Build 20992742862

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • 7 unchanged lines in 2 files lost coverage.
  • Overall coverage remained the same at 56.148%

Files with Coverage Reduction New Missed Lines %
internal/storage/rm/rm.go 2 80.61%
internal/gen/keys/keys.go 5 12.9%
Totals Coverage Status
Change from base Build 20983346857: 0.0%
Covered Lines: 6850
Relevant Lines: 12200

💛 - Coveralls

@joelpramos
Copy link

joelpramos commented Feb 15, 2026

@kallebysantos Deno.env.get doesn't seem to have access to "SUPABASE_INTERNAL_*" for me. Is there any property that needs enablement?

const JWT_SECRET = Deno.env.get('SUPABASE_INTERNAL_JWT_SECRET')!;
const HOST_PORT = Deno.env.get('SUPABASE_INTERNAL_HOST_PORT')!;
const DEBUG = Deno.env.get('SUPABASE_INTERNAL_DEBUG') === 'true';
const FUNCTIONS_CONFIG_STRING = Deno.env.get(
  'SUPABASE_INTERNAL_FUNCTIONS_CONFIG'
)!;

async function verifyLegacyJWT(
  jwt: string
): Promise<JWTVerifyResult<JWTPayload> | undefined> {
  // const JWT_SECRET = Deno.env.get('SUPABASE_INTERNAL_JWT_SECRET')!;
  logger.debug('JWT secret: ' + JWT_SECRET);
  logger.debug('Host port: ' + HOST_PORT);
  logger.debug('Debug: ' + DEBUG);
  logger.debug('Functions config string: ' + FUNCTIONS_CONFIG_STRING);
  const encoder = new TextEncoder();
  const secretKey = encoder.encode(JWT_SECRET);
  try {
    return await jwtVerify(jwt, secretKey);
  } catch (e) {
    logger.error('Symmetric Legacy JWT verification error', e);
    return undefined;
  }
}
image

@kallebysantos
Copy link
Member Author

Hi @joelpramos 💚
Could you give me more context about your version? Are you calling it inside an edge function or just trying my fork?

@joelpramos
Copy link

hi @kallebysantos:

  • Supabase version 2.75.0
  • Local (self hosted)
  • Just trying to replicate your middleware change, not using your fork
  • Getting the var from within a Supabase edge function (my middleware, trying to replicate what you have here as I have this problem locally)
  • I can see the vars injected in the container itself and confirmed by opening the shell and printing it

@kallebysantos
Copy link
Member Author

kallebysantos commented Feb 16, 2026

Hey @joelpramos 💚
The SUPABASE_INTERNAL_* are only available inside CLI context as internal envs for the docker local-dev stack — So it only works in this fork / supabase start command (only for internal main.ts scope)

Since you're using self-hosted, the main/index.ts template already implements the legacy verification and the env is named as JWT_SECRET.

I'm not really sure, but new JWK are not available for self-hosting yet, so you "can't" replicate this hybrid replication — At least without extra manual steps.

cc @aantti can give you more details about Self-Host situation of new Asymmetric Keys.

@aantti
Copy link

aantti commented Feb 16, 2026

can give you more details about Self-Host situation of new Asymmetric Keys

It's WIP :) Hoping to add it soon.

@joelpramos
Copy link

@aantti @kallebysantos at least locally the new solution works totally fine for users but not for service_user (e.g., functions triggered from Cron). I am also having some issues in the deployed instance with functions triggered from cron jobs but haven't triaged yet the root cause and missing some logs.

fwiw other than the fact the variable wasn't available, this hybrid solution you posted @kallebysantos works for me i.e. seems like the service_role from the cron user is using the old auth method. Is that something on both your radars?

@kallebysantos
Copy link
Member Author

Hey @joelpramos

service_role...is using the old auth method

Yes, ANON_KEY and SERVICE_ROLE uses old auth (symmetric JWT) which can be verified from *JWT_SECRET

...fine for users but not for the service_role...

I don't think so, user's tokens is now being issued as new JWT (Asymmetric JWK) and the purpose of this PR is to handle both kinds of tokens, using legacy as fallback.
So it should work for any valid token... but I can do more testing to make sure if cron requests is working fine.

It helps to reduce latency for Legacy token verifications, since it
avoid unnecessary requests.
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved JWT token verification with support for both legacy and modern authentication methods for enhanced compatibility.
    • Enhanced error messaging during token verification for better troubleshooting.

Walkthrough

The pull request updates JWT verification in Edge Functions by switching from the legacy jose library to jsr:@panva/jose`` and implementing a hybrid verification strategy. The change introduces a new verifyHybridJWT() function that inspects the JWT header algorithm and routes verification to either a legacy symmetric HS256 path or an asymmetric ES256/RS256 path using a remote JWKS endpoint. The verification endpoint is derived from the `SUPABASE_URL` environment variable, and the logic includes lazy-loading of JWKS to optimize performance.

Sequence Diagram(s)

sequenceDiagram
    participant RequestHandler as Request Handler
    participant VerifyHybrid as verifyHybridJWT()
    participant AlgoCheck as Algorithm<br/>Checker
    participant LegacyPath as isValidLegacyJWT()
    participant JWKSPath as isValidJWT()
    participant JWKSEndpoint as JWKS Endpoint

    RequestHandler->>VerifyHybrid: verifyHybridJWT(JWT_SECRET, JWKS_ENDPOINT, token)
    VerifyHybrid->>AlgoCheck: Extract algorithm from<br/>JWT header
    
    alt Algorithm is HS256
        AlgoCheck->>LegacyPath: Route to legacy<br/>verification
        LegacyPath->>LegacyPath: Verify with JWT_SECRET<br/>(symmetric)
        LegacyPath-->>VerifyHybrid: Return result
    else Algorithm is ES256 or RS256
        AlgoCheck->>JWKSPath: Route to JWKS<br/>verification
        JWKSPath->>JWKSEndpoint: Lazy-load JWKS<br/>(if not cached)
        JWKSEndpoint-->>JWKSPath: Return public keys
        JWKSPath->>JWKSPath: Verify with JWKS<br/>(asymmetric)
        JWKSPath-->>VerifyHybrid: Return result
    end
    
    VerifyHybrid-->>RequestHandler: Return verification<br/>result
Loading

Assessment against linked issues

Objective Addressed Explanation
Fix JWT key verification broken in Edge Runtime 1.7.0+ where ES256 verification fails with CryptoKey/Uint8Array type mismatch [#654]
Support both symmetric (HS256) and asymmetric (ES256/RS256) JWT verification paths for self-hosted deployments [#654]
Implement lazy-loading of JWKS from remote endpoint to avoid repeated fetches [#654]

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JWT key verification broken (Edge Runtime 1.7.0+) for self-hosted deployments

4 participants

Comments