Skip to content

Switched from oslo to jose#520

Open
markusahlstrand wants to merge 2 commits intomainfrom
jose
Open

Switched from oslo to jose#520
markusahlstrand wants to merge 2 commits intomainfrom
jose

Conversation

@markusahlstrand
Copy link
Copy Markdown
Owner

@markusahlstrand markusahlstrand commented Feb 2, 2026

Summary by CodeRabbit

  • Refactor
    • Reworked token signing/verification internals to a new crypto backend (no public API changes).
  • New Features
    • Added internal utilities for encoding/decoding, hashing, PKCE code verifier generation, and origin verification.
  • Tests
    • Updated test fixtures and token helpers to match the new token handling.
  • Chores
    • Database migration: universal login timestamp columns changed to bigint.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
authero-docs Ready Ready Preview, Comment Feb 2, 2026 8:23am
authhero-react-admin Ready Ready Preview, Comment Feb 2, 2026 8:23am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

This PR migrates authhero from oslo to jose for JWT signing/decoding, adds a local encoding utility to replace oslo encoding/crypto helpers, and updates token creation, parsing, strategies, routes, and tests accordingly.

Changes

Cohort / File(s) Summary
Release Declaration
.changeset/beige-parrots-fail.md
Add changeset declaring a minor release with description "Switch to use jose".
Dependency Update
packages/authhero/package.json
Remove oslo and add jose (^6.0.0).
Encoding Utilities (new)
packages/authhero/src/utils/encoding.ts
Add local encoding module: hex/base64/base64url encode/decode, sha256, verifyRequestOrigin, generateCodeVerifier, and PKCE helpers using Web Crypto APIs.
Core JWT Signing
packages/authhero/src/authentication-flows/common.ts
Replace oslo JWT creation with jose SignJWT/importPKCS8; set issued/expiry via jose API and set protected header (alg RS256, kid).
Strategies — ID token parsing
packages/authhero/src/strategies/apple.tsx, .../google-oauth2.tsx, .../microsoft.tsx, .../oidc.tsx, .../vipps.tsx
Replace oslo parseJWT with jose.decodeJwt; use decoded payload directly and simplify payload extraction/variable names.
Route handlers
packages/authhero/src/routes/auth-api/account.ts, packages/authhero/src/routes/auth-api/authorize.ts
Swap imports to local encoding utilities (verifyRequestOrigin, decodeBase64url) instead of oslo.
Crypto/encryption utils
packages/authhero/src/utils/crypto.ts, packages/authhero/src/utils/encryption.ts
Switch encoding imports to local ./encoding; update thumbprint and PEM/base64 handling to use new helpers.
Test helpers — token creation
packages/authhero/test/helpers/token.ts
Replace test JWT creation from oslo to jose SignJWT/importPKCS8; remove pemToBuffer; adapt issued/expiry and header usage.
Tests — token parsing & utilities
packages/authhero/test/.../*.spec.ts (multiple files: authentication-flows/*, routes/*, hooks/*, universal-login/*, ticket.spec.ts, etc.)
Replace oslo parseJWT/base64/oauth2 imports with jose.decodeJwt and local encoding utilities; adjust tests to access claims directly on decoded token (no nested payload).
Test configuration
packages/authhero/test/helpers/test-server.ts
Extend test client's web_origins to include http://localhost:3000 in addition to https://example.com.
DB migration (kysely)
packages/kysely/migrate/..._universal_login_templates.ts
Change two timestamp columns from integer to bigint and swap addition order (created_at_ts before updated_at_ts).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • Openid basic tests #496: Modifies authhero authentication flows in src/authentication-flows/common.ts, overlapping JWT creation/signing changes.
  • Vipps #18: Touches the Vipps strategy and ID token handling (src/strategies/vipps.tsx), related to decode/sign changes.

Poem

🐇 I hopped through code with tiny paws,
I swapped old oslo for jose's laws,
Tokens signed with nimble cheer,
Base64 neat and errors clear,
A rabbit's hop — secure and spry!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: replacing the oslo library with jose for JWT and crypto operations across the authhero package.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jose

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/authhero/src/strategies/apple.tsx`:
- Line 6: The code currently decodes the Apple ID token with decodeJwt() and
parses its shape via idTokenSchema.parse(), but does not verify the JWT
signature or validate claims; update the flow after
apple.validateAuthorizationCode(code) so you fetch Apple's OIDC config and JWKS
(from /.well-known/openid-configuration), use jose.jwtVerify() with the correct
JWK (or a jose.createRemoteJWKSet) to verify tokens.idToken() signature, then
parse/assert the verified payload and enforce claims (iss ===
"https://appleid.apple.com", aud matches your client ID, exp not expired, and
the expected nonce) before using the payload in downstream logic (replace the
decodeJwt + idTokenSchema.parse usage).

In `@packages/authhero/src/strategies/google-oauth2.tsx`:
- Line 6: The code uses decodeJwt to parse the Google ID token (see decodeJwt
and idTokenSchema.parse) but does not verify the JWS signature or standard
claims; replace the unsafe decode-only flow by performing full verification with
jose.jwtVerify (or equivalent) against Google's JWKS and validate iss, aud and
exp claims (or call/extend validateAuthorizationCode to perform this server-side
verification). Specifically, fetch/refresh Google's JWKS, call
jwtVerify(id_token, key, { issuer: "https://accounts.google.com" |
"accounts.google.com", audience: expectedClientId }), check expiration and nonce
if used, and only pass the verified payload into idTokenSchema.parse; apply same
pattern to other OIDC strategies (Apple, Microsoft) that currently use
decodeJwt.

In `@packages/authhero/src/strategies/vipps.tsx`:
- Around line 100-103: Wrap the call to decodeJwt(tokens.idToken()) in a
try-catch and map any decode errors to a controlled 400 response; specifically,
catch exceptions thrown when computing idTokenPayload and return or throw a
BadRequest/ClientError instead of letting the error bubble as a 500, then only
call idTokenSchema.parse(idTokenPayload) when decoding succeeds. Update the
logic around decodeJwt, idTokenPayload, and idTokenSchema.parse to ensure
malformed ID tokens produce a clear 400 error with an appropriate message.
🧹 Nitpick comments (1)
packages/authhero/test/routes/auth-api/token.spec.ts (1)

170-173: Consider using more specific type assertions instead of any.

Throughout the file, as any is used to access custom JWT claims. Line 1059 shows a better pattern with as { scope?: string }. While as any works fine in tests, using specific types improves readability and catches typos.

♻️ Optional: Use specific type assertions
-      const payload = accessToken as any;
+      const payload = accessToken as { scope?: string };
       expect(payload.scope).toContain("read:users");

Similar pattern could be applied elsewhere, e.g., for tokens with permissions:

const payload = accessToken as { scope?: string; permissions?: string[]; org_id?: string };

import { nanoid } from "nanoid";
import { Bindings, Variables } from "../types";
import { parseJWT } from "oslo/jwt";
import { decodeJwt } from "jose";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd packages/authhero && wc -l src/strategies/apple.tsx

Repository: markusahlstrand/authhero

Length of output: 96


🏁 Script executed:

cd packages/authhero && head -150 src/strategies/apple.tsx | tail -145

Repository: markusahlstrand/authhero

Length of output: 3974


🏁 Script executed:

cd packages/authhero && sed -n '100,115p' src/strategies/apple.tsx

Repository: markusahlstrand/authhero

Length of output: 485


🏁 Script executed:

cd packages/authhero && find . -name "*IdToken*" -type f

Repository: markusahlstrand/authhero

Length of output: 90


🏁 Script executed:

cd packages/authhero && rg "idTokenSchema" -A 5

Repository: markusahlstrand/authhero

Length of output: 3993


🏁 Script executed:

cd packages/authhero && rg "arctic" package.json -A 1 -B 1

Repository: markusahlstrand/authhero

Length of output: 149


🏁 Script executed:

cd packages/authhero && cat src/types/IdToken.ts

Repository: markusahlstrand/authhero

Length of output: 1085


🏁 Script executed:

cd packages/authhero && sed -n '100,118p' src/strategies/apple.tsx

Repository: markusahlstrand/authhero

Length of output: 520


🏁 Script executed:

npm info arctic 2>/dev/null | grep -A 20 "description\|version"

Repository: markusahlstrand/authhero

Length of output: 628


🏁 Script executed:

cd packages/authhero && grep -r "validateAuthorizationCode" . --include="*.ts" --include="*.tsx" -B 2 -A 5 | head -80

Repository: markusahlstrand/authhero

Length of output: 4189


🌐 Web query:

arctic npm library validateAuthorizationCode ID token validation signature

💡 Result:

  • validateAuthorizationCode() in Arctic: This method exchanges the OAuth/OIDC authorization code for tokens (access token, and ID token when the provider returns one). It’s not an ID-token cryptographic verifier; it’s the token endpoint “code → tokens” step. [1]
  • ID token handling in Arctic: Arctic provides decodeIdToken() only to decode the ID token payload and explicitly states it does not validate the signature. [2]

What to do for ID token signature validation

Use a JWT/OIDC verification library (commonly jose) and verify against the provider’s JWKS (from its OIDC discovery metadata), e.g.:

  • fetch issuer metadata (/.well-known/openid-configuration)
  • use jwks_uri to verify the JWT signature
  • enforce claims: iss, aud, exp, optionally nonce

Arctic’s role here is: get idToken = tokens.idToken(), then you verify it yourself. [1][2]

Sources
[1] Arctic v2 OAuth 2.0 guide (Validate authorization code / OAuth2Tokens, idToken()) (v2.arcticjs.dev)
[2] Arctic API reference: decodeIdToken() (“does not validate the signature”) (arcticjs.dev)

Citations:


Add ID token signature verification and claims validation.

The code decodes the Apple ID token but does not verify its JWT signature or validate critical claims (iss, aud, exp, nonce). decodeJwt() only decodes without verification, and idTokenSchema.parse() only checks structure, not correctness of values.

To fix: Fetch Apple's JWKS from their OIDC discovery endpoint (/.well-known/openid-configuration), verify the JWT signature using jose.jwtVerify() with the JWKS, and enforce claim validation (issuer, audience, expiration, nonce).

Lines 105–107
const tokens = await apple.validateAuthorizationCode(code);
const idTokenPayload = decodeJwt(tokens.idToken());
const payload = idTokenSchema.parse(idTokenPayload);
🤖 Prompt for AI Agents
In `@packages/authhero/src/strategies/apple.tsx` at line 6, The code currently
decodes the Apple ID token with decodeJwt() and parses its shape via
idTokenSchema.parse(), but does not verify the JWT signature or validate claims;
update the flow after apple.validateAuthorizationCode(code) so you fetch Apple's
OIDC config and JWKS (from /.well-known/openid-configuration), use
jose.jwtVerify() with the correct JWK (or a jose.createRemoteJWKSet) to verify
tokens.idToken() signature, then parse/assert the verified payload and enforce
claims (iss === "https://appleid.apple.com", aud matches your client ID, exp not
expired, and the expected nonce) before using the payload in downstream logic
(replace the decodeJwt + idTokenSchema.parse usage).

import { nanoid } from "nanoid";
import { Bindings, Variables } from "../types";
import { parseJWT } from "oslo/jwt";
import { decodeJwt } from "jose";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to see the full context around the import and usage
cat -n packages/authhero/src/strategies/google-oauth2.tsx | head -120

Repository: markusahlstrand/authhero

Length of output: 4411


🏁 Script executed:

# Also check if there are other google oauth related files to understand the architecture
fd "google" packages/authhero/src --type f

Repository: markusahlstrand/authhero

Length of output: 166


🏁 Script executed:

# Search for any validation of ID tokens in the file
rg -n "idToken|jwtVerify|verify" packages/authhero/src/strategies/google-oauth2.tsx -A 3 -B 3

Repository: markusahlstrand/authhero

Length of output: 583


🏁 Script executed:

# Find and read the IdToken type definition
cat -n packages/authhero/src/types/IdToken.ts 2>/dev/null || cat -n packages/authhero/src/types/IdToken.tsx 2>/dev/null || fd "IdToken" packages/authhero/src/types --type f

Repository: markusahlstrand/authhero

Length of output: 1330


🏁 Script executed:

# Search for any JWT verification patterns in the codebase
rg "jwtVerify|verifyJwt" packages/authhero --type ts --type tsx

Repository: markusahlstrand/authhero

Length of output: 95


🏁 Script executed:

# Check if arctic library validates ID tokens by looking at other oauth strategies
rg -A 10 "validateAuthorizationCode" packages/authhero/src/strategies --type ts --type tsx | head -50

Repository: markusahlstrand/authhero

Length of output: 95


🏁 Script executed:

# Search for any token validation or verification in the codebase
rg "verify|validate" packages/authhero/src/strategies/google-oauth2.tsx -i

Repository: markusahlstrand/authhero

Length of output: 204


🏁 Script executed:

# Check if there's documentation or comments about arctic library validation
rg "arctic" packages/authhero -A 5 -B 5 | head -80

Repository: markusahlstrand/authhero

Length of output: 5651


🏁 Script executed:

# Look for any JWT verification patterns or documentation in the repo
rg -i "signature|verify.*jwt|jwtverify" packages/authhero --type ts --type-list | head -40

Repository: markusahlstrand/authhero

Length of output: 457


🏁 Script executed:

# Check package.json to see the arctic version and any security notes
cat packages/authhero/package.json | grep -A 5 -B 5 "arctic"

Repository: markusahlstrand/authhero

Length of output: 362


🏁 Script executed:

# Search for any other OAuth strategies to see how they handle token validation
fd "oauth|oidc" packages/authhero/src/strategies --type f

Repository: markusahlstrand/authhero

Length of output: 204


🏁 Script executed:

# Check how other strategies handle ID token validation, particularly OIDC
cat -n packages/authhero/src/strategies/oidc.tsx | head -130 | tail -60

Repository: markusahlstrand/authhero

Length of output: 2062


🏁 Script executed:

# Check if there's any jwtVerify usage in OIDC or other files
rg "jwtVerify" packages/authhero/src --type ts -B 5 -A 5

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Look for any JWKS or key validation logic
rg -i "jwks|publickey|jwtverify|signature" packages/authhero/src --type ts | head -20

Repository: markusahlstrand/authhero

Length of output: 2000


🏁 Script executed:

# Check Arctic library's implementation by looking at node_modules or any docs
find node_modules/arctic -name "*.d.ts" 2>/dev/null | head -5

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check if Arctic library validates ID tokens by looking at type definitions
ls -la node_modules/arctic/ 2>/dev/null | head -20

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check if there's any recent commit or documentation about arctic token validation
rg -i "arctic.*validate\|verify.*token" packages/authhero --type ts -B 2 -A 2

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Let's verify if jwtVerify is actually being used anywhere for ID tokens
rg "jwtVerify" packages/authhero/src --type ts -B 3 -A 3

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check the Google strategy more carefully - does validateAuthorizationCode return validated tokens?
cat -n packages/authhero/src/strategies/google-oauth2.tsx | sed -n '76,110p'

Repository: markusahlstrand/authhero

Length of output: 1213


🏁 Script executed:

# Search for any documentation or examples of how Arctic handles token validation
grep -r "validateAuthorizationCode" packages/authhero/src --type ts -A 10 | head -50

Repository: markusahlstrand/authhero

Length of output: 185


🏁 Script executed:

# Check if there's a different pattern used for signature verification anywhere
rg "verifySignature|importSPKI|importPKCS8|jwtVerify" packages/authhero --type ts

Repository: markusahlstrand/authhero

Length of output: 480


🏁 Script executed:

# Look at the jose library usage - what functions are imported from it
rg "from \"jose\"" packages/authhero/src --type ts

Repository: markusahlstrand/authhero

Length of output: 561


🏁 Script executed:

# Let's check if there's any validation happening after validateAuthorizationCode that we might have missed
rg "validateAuthorizationCode" packages/authhero/src/strategies -A 15 | grep -E "(verify|validate|check)"

Repository: markusahlstrand/authhero

Length of output: 2316


🏁 Script executed:

# Search for any comments or TODOs about token validation
rg -i "token.*verify|verify.*token|jwt.*verify|todo.*verify" packages/authhero/src/strategies --type ts

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check Apple strategy to see if there's any difference in how it handles token validation
cat -n packages/authhero/src/strategies/apple.tsx | head -120

Repository: markusahlstrand/authhero

Length of output: 4800


🏁 Script executed:

# Check the Microsoft strategy as well to see the pattern
cat -n packages/authhero/src/strategies/microsoft.tsx | head -120

Repository: markusahlstrand/authhero

Length of output: 3652


🏁 Script executed:

# Let's look for any Arctic library documentation or TypeScript definitions in the project
find . -name "*.d.ts" -path "*/arctic/*" 2>/dev/null | head -10

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check if there's any reference to token validation in comments or documentation
rg -i "arctic.*token.*verify\|id.*token.*valid" packages/authhero --type ts -B 3 -A 3

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Look for any integration tests or examples that might show how Arctic handles token validation
fd "test\|spec" packages/authhero/src/strategies --type f

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Search for any JWKS or public key usage that might indicate server-side verification
cat -n packages/authhero/src/utils/jwt.ts | head -80

Repository: markusahlstrand/authhero

Length of output: 2617


🏁 Script executed:

# Check if there are tests that might show how tokens are validated
find packages/authhero -name "*.test.*" -o -name "*.spec.*" | head -10

Repository: markusahlstrand/authhero

Length of output: 660


🏁 Script executed:

# Let's check if Arctic provides any public key or signature validation
rg "arctic" packages/authhero/package.json

Repository: markusahlstrand/authhero

Length of output: 90


🏁 Script executed:

# Look for the actual usage patterns - are there comments about security?
grep -n "validateAuthorizationCode\|decodeJwt\|idTokenSchema" packages/authhero/src/strategies/google-oauth2.tsx -B 5 -A 5

Repository: markusahlstrand/authhero

Length of output: 1234


🏁 Script executed:

# Let's search for any security concerns or notes in the codebase about token validation
rg -i "signature|forged|trust.*token" packages/authhero/src --type ts | head -20

Repository: markusahlstrand/authhero

Length of output: 668


🏁 Script executed:

# Check if there's documentation about Arctic library's security model
cat packages/authhero/package.json | head -40

Repository: markusahlstrand/authhero

Length of output: 1259


🏁 Script executed:

# Let's check the git history to see if there were any discussions about token validation
git log --oneline packages/authhero/src/strategies/google-oauth2.tsx 2>/dev/null | head -20

Repository: markusahlstrand/authhero

Length of output: 94


🏁 Script executed:

# Check if there's a CHANGELOG or SECURITY.md file
find . -name "CHANGELOG*" -o -name "SECURITY*" -o -name "SECURITY.md" | head -5

Repository: markusahlstrand/authhero

Length of output: 224


🏁 Script executed:

# Let's look for any nonce validation since the schema includes nonce
rg "nonce" packages/authhero/src/strategies --type ts -B 3 -A 3

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check if validateAuthorizationCode returns any verified tokens by looking at usage
rg "validateAuthorizationCode" packages/authhero/src -A 20 | head -60

Repository: markusahlstrand/authhero

Length of output: 4207


🏁 Script executed:

# Let's check what Arctic's validateAuthorizationCode returns
# by looking at how the tokens object is used
rg "tokens\." packages/authhero/src/strategies/google-oauth2.tsx

Repository: markusahlstrand/authhero

Length of output: 121


🏁 Script executed:

# Let's search for any HTTP calls or server-side token validation that happens before decodeJwt
rg -B 10 "decodeJwt" packages/authhero/src/strategies/google-oauth2.tsx

Repository: markusahlstrand/authhero

Length of output: 592


🏁 Script executed:

# Let's check if there's any issuer validation or provider-specific validation
rg -i "issuer\|provider\|validate.*iss\|check.*iss" packages/authhero/src --type ts | head -20

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Final check: does Arctic make any server-side validation calls?
# Let's look for fetch calls or token endpoint calls in the Google strategy
rg "fetch\|http\|request" packages/authhero/src/strategies/google-oauth2.tsx

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Let's do a final check - look for any tests that might show how tokens are validated
grep -r "validateAuthorizationCode\|idToken" packages/authhero/test --include="*.ts" --include="*.tsx" 2>/dev/null | head -20

Repository: markusahlstrand/authhero

Length of output: 769


🏁 Script executed:

# Let's verify what jose's decodeJwt actually does vs jwtVerify
web_search

Repository: markusahlstrand/authhero

Length of output: 118


🌐 Web query:

jose library decodeJwt vs jwtVerify signature validation

💡 Result:

In panva/jose:

  • decodeJwt(jwt): only decodes the JWT Claims Set (payload) so you can inspect it before validation. It does not perform cryptographic integrity/signature validation (and it’s not a replacement for verification). Use it only for non-security decisions like picking a key id (kid) or debugging. [1]

  • jwtVerify(jwt, key, options): performs JWS signature verification and JWT Claims Set validation (e.g., time-based claims like exp/nbf, and other checks depending on provided options). Use this when you actually need to trust the token. [1][2]

If your question is specifically “does it validate the signature?”: decodeJwt = no, jwtVerify = yes. [1][2]

Sources:
[1] (app.unpkg.com)
[2] (github.com)

Citations:


🌐 Web query:

arctic validateAuthorizationCode Google token validation

💡 Result:

In Arctic, validateAuthorizationCode() is not “token validation” (signature/claims verification). It exchanges an OAuth 2.0 authorization code for tokens by calling the provider’s token endpoint (“code → tokens”). (arcticjs.dev)

Google + Arctic: exchange the code

Use the Google provider and call:

import * as arctic from "arctic";

const tokens = await google.validateAuthorizationCode(code, codeVerifier);
const accessToken = tokens.accessToken();
const idToken = tokens.idToken(); // if you requested "openid"
const claims = arctic.decodeIdToken(idToken); // decode only

Arctic’s Google guide shows this flow and recommends OIDC (openid, plus profile/email if needed). (arcticjs.dev)

If you mean “validate the Google token” (ID token verification)

To verify a Google ID token in production, Google recommends verifying:

  • JWT signature using Google public keys (from the OIDC discovery/JWKS),
  • iss, aud, and exp claims (and hd if you restrict to Workspace domains). (developers.google.com)

Google’s tokeninfo endpoint can validate for debugging, but it’s not recommended for production (can be throttled / intermittent). (developers.google.com)

Citations:


Verify and validate ID token signatures before trusting the decoded payload.

decodeJwt only decodes the JWT without validating its signature. While idTokenSchema.parse checks field structure, it does NOT verify JWT signature, issuer, audience, or expiration validity. According to Google's documentation, production deployments must verify the ID token's JWS signature against Google's public keys (via JWKS), and validate iss, aud, and exp claims. Without this verification, forged tokens can bypass authentication.

Consider using jwtVerify from jose with proper claim validation, or ensure Arctic's validateAuthorizationCode already performs server-side ID token verification (currently it only exchanges the code for tokens, not validate them).

This also applies to other OIDC strategies (Apple, Microsoft) using the same decoding pattern.

🤖 Prompt for AI Agents
In `@packages/authhero/src/strategies/google-oauth2.tsx` at line 6, The code uses
decodeJwt to parse the Google ID token (see decodeJwt and idTokenSchema.parse)
but does not verify the JWS signature or standard claims; replace the unsafe
decode-only flow by performing full verification with jose.jwtVerify (or
equivalent) against Google's JWKS and validate iss, aud and exp claims (or
call/extend validateAuthorizationCode to perform this server-side verification).
Specifically, fetch/refresh Google's JWKS, call jwtVerify(id_token, key, {
issuer: "https://accounts.google.com" | "accounts.google.com", audience:
expectedClientId }), check expiration and nonce if used, and only pass the
verified payload into idTokenSchema.parse; apply same pattern to other OIDC
strategies (Apple, Microsoft) that currently use decodeJwt.

Comment on lines +100 to 103
const idTokenPayload = decodeJwt(tokens.idToken());

if (!idToken) {
throw new Error("Invalid ID token");
}

const payload = idTokenSchema.parse(idToken.payload);
const payload = idTokenSchema.parse(idTokenPayload);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the vipps.tsx file to understand the context
wc -l packages/authhero/src/strategies/vipps.tsx

Repository: markusahlstrand/authhero

Length of output: 114


🏁 Script executed:

# Read the vipps.tsx file around the mentioned lines
sed -n '90,115n' packages/authhero/src/strategies/vipps.tsx

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Search for how decodeJwt is imported and used across the codebase
rg "decodeJwt" --type ts --type tsx -B 2 -A 2

Repository: markusahlstrand/authhero

Length of output: 95


🏁 Script executed:

# Check if there's already error handling for similar JWT operations
rg "JSONHTTPException" packages/authhero/src/strategies/ -A 2 -B 2

Repository: markusahlstrand/authhero

Length of output: 3135


🌐 Web query:

jose library JavaScript decodeJwt error handling invalid malformed JWT tokens

💡 Result:

In jose, decodeJwt(token) is a parsing utility (it does not verify signatures or validate JWT claims), so you should treat it as “best-effort decode” and wrap it in try/catch. If you need actual validation, use jwtVerify() instead (it performs signature verification and JWT Claims Set validation). [1]

Practical error-handling pattern for decodeJwt

import * as jose from 'jose';

function safeDecodeJwt(token) {
  try {
    return { ok: true, payload: jose.decodeJwt(token) };
  } catch (err) {
    // jose errors expose stable error codes and can be checked via `err.code`
    // or with `instanceof jose.errors.SomeErrorClass`
    return {
      ok: false,
      code: err?.code,          // e.g. ERR_JWT_...
      message: err?.message,
    };
  }
}

What typically makes a token “malformed”

A JWT must be in the compact format header.payload.signature (3 dot-separated parts). If the token is missing a part, corrupted, etc., decoding fails. [2] Tokens that are not strictly base64url-encoded can also be rejected (historically resulting in a JWTMalformed-type failure). [3]

If the token is untrusted: verify instead of decode

If the goal is “reject invalid/malformed/expired/tampered tokens”, use jwtVerify() and handle its thrown errors the same way (by err.code and/or instanceof). [1][4]

try {
  const { payload, protectedHeader } = await jose.jwtVerify(token, keyOrGetKey, {
    issuer: 'https://issuer.example',
    audience: 'my-aud',
  });
} catch (err) {
  // err.code is the recommended stable discriminator
}

Sources: [1] [2] [3] [4]


🏁 Script executed:

# Get more context around lines 100-103 to see the full flow
cat -n packages/authhero/src/strategies/vipps.tsx | sed -n '85,110p'

Repository: markusahlstrand/authhero

Length of output: 916


🏁 Script executed:

# Check if there are other JWT decode operations in strategies to see existing patterns
rg "decodeJwt\|jwtVerify" packages/authhero/src/strategies/ -B 3 -A 3

Repository: markusahlstrand/authhero

Length of output: 50


🏁 Script executed:

# Check the types to understand idTokenSchema and what validation it does
rg "idTokenSchema" packages/authhero/src/ -B 2 -A 5

Repository: markusahlstrand/authhero

Length of output: 7242


Wrap decodeJwt in try-catch to handle malformed ID tokens.

decodeJwt from the jose library throws on invalid or malformed tokens. This should be caught and mapped to a controlled 400 error response instead of propagating as a 500.

Suggested fix
-  const idTokenPayload = decodeJwt(tokens.idToken());
+  let idTokenPayload;
+  try {
+    idTokenPayload = decodeJwt(tokens.idToken());
+  } catch {
+    throw new JSONHTTPException(400, { message: "Invalid ID token" });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const idTokenPayload = decodeJwt(tokens.idToken());
if (!idToken) {
throw new Error("Invalid ID token");
}
const payload = idTokenSchema.parse(idToken.payload);
const payload = idTokenSchema.parse(idTokenPayload);
let idTokenPayload;
try {
idTokenPayload = decodeJwt(tokens.idToken());
} catch {
throw new JSONHTTPException(400, { message: "Invalid ID token" });
}
const payload = idTokenSchema.parse(idTokenPayload);
🤖 Prompt for AI Agents
In `@packages/authhero/src/strategies/vipps.tsx` around lines 100 - 103, Wrap the
call to decodeJwt(tokens.idToken()) in a try-catch and map any decode errors to
a controlled 400 response; specifically, catch exceptions thrown when computing
idTokenPayload and return or throw a BadRequest/ClientError instead of letting
the error bubble as a 500, then only call idTokenSchema.parse(idTokenPayload)
when decoding succeeds. Update the logic around decodeJwt, idTokenPayload, and
idTokenSchema.parse to ensure malformed ID tokens produce a clear 400 error with
an appropriate message.

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.

1 participant