Skip to content

feat: add jwt_es256 auth type for signed JWT APIs#47

Merged
donna-matt merged 4 commits intomainfrom
feat/jwt-es256-auth
Mar 30, 2026
Merged

feat: add jwt_es256 auth type for signed JWT APIs#47
donna-matt merged 4 commits intomainfrom
feat/jwt-es256-auth

Conversation

@donna-matt
Copy link
Copy Markdown
Collaborator

What

New jwt_es256 authentication method type that dynamically generates ES256-signed JWTs for each proxied request.

Why

APIs like Apple's App Store Connect require fresh JWT tokens signed with ECDSA P-256 per request, rather than static bearer tokens. Each JWT must be signed using:

  • A private key (PEM-encoded PKCS#8)
  • A key ID (kid in JWT header)
  • An issuer ID (iss in JWT payload)

Previously, there was no way to handle this authentication pattern in Shellgate.

How It Works

Backend Implementation

  1. New JWT utility (src/lib/server/utils/jwt.ts):

    • Pure Web Crypto API implementation (no external dependencies)
    • Signs ES256 JWTs using ECDSA with P-256 curve
    • Configurable audience and expiration time
    • Default: appstoreconnect-v1 audience, 20-minute expiration
  2. Updated auth methods service (src/lib/server/services/auth-methods.ts):

    • Added jwt_es256 to valid auth types
    • Credential hint displays key ID: ES256 JWT ••• KEYID123
  3. Updated gateway proxy (src/lib/server/services/gateway.ts):

    • Generates fresh JWT for each request when jwt_es256 auth method is configured
    • Injects as Authorization: Bearer <jwt> header

Testing

  • Unit tests (tests/unit/jwt.test.ts): 7 tests covering JWT generation, signature verification, custom config
  • Integration tests (tests/integration/gateway.test.ts): End-to-end test verifying JWT injection in proxied requests
  • All existing tests pass ✅

Usage Example

Creating a Target + Auth Method for App Store Connect

  1. Create target:
POST /api/targets
{
  "name": "Apple App Store Connect",
  "slug": "apple-asc",
  "type": "api",
  "base_url": "https://api.appstoreconnect.apple.com"
}
  1. Create auth method:
POST /api/targets/{target_id}/auth-methods
{
  "label": "App Store Connect JWT",
  "type": "jwt_es256",
  "credential": "{\"privateKey\":\"-----BEGIN PRIVATE KEY-----\\nMIGHAgEAMB...\",\"keyId\":\"ABC123XYZ\",\"issuerId\":\"69a6de12-b0d3-...\"}"
}
  1. Make requests:
GET /gateway/apple-asc/v1/apps
# Shellgate automatically generates and injects fresh JWT

Credential Format

The credential field must be a JSON string with:

{
  "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGH...\n-----END PRIVATE KEY-----",
  "keyId": "ABC123XYZ",
  "issuerId": "69a6de12-b0d3-...",
  "audience": "appstoreconnect-v1",  // optional, defaults to appstoreconnect-v1
  "expiresInSeconds": 1200              // optional, defaults to 1200 (20 minutes)
}

Dashboard UI

Dashboard UI for creating/editing jwt_es256 credentials is intentionally not included in this PR. This keeps the PR focused on backend functionality. Dashboard support will be added in a follow-up PR.

Security Notes

  • Private keys are stored encrypted in the database (existing credential encryption)
  • JWTs are generated fresh per request (no token reuse)
  • Default 20-minute expiration minimizes token lifetime
  • Uses Web Crypto API (native, audited implementation)

Breaking Changes

None. This is purely additive — existing auth methods continue to work unchanged.

donna-matt and others added 4 commits March 30, 2026 11:04
Buffer.from().buffer returns the entire pooled ArrayBuffer (8KB) instead
of just the key bytes, causing crypto.subtle.importKey to fail. Also made
PEM parsing more tolerant of varying dash counts and escaped newlines,
and added error handling for descriptive JWT error messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@donna-matt donna-matt merged commit 2ceffd1 into main Mar 30, 2026
donna-matt added a commit that referenced this pull request Apr 1, 2026
- Add webhookKey and webhookSecret columns to tokens table
- New inbound_events table with status/expiry/channel fields
- POST /inbound/{webhookKey}/{channel} — receive webhooks from external services
- GET /inbound/pending — poll pending events (bearer auth)
- POST /inbound/ack/{id} — acknowledge processed events
- DELETE /inbound/purge — admin purge old events
- POST/DELETE/PATCH /api/tokens/{id}/webhook — manage webhook keys
- In-memory rate limiter (100 req/min per key)
- HMAC-SHA256 signature verification (GitHub/Linear/Stripe style)
- Dashboard UI section in api-keys detail page
- Migration: drizzle/0005_inbound_webhook_buffer.sql
- Tests: 16 new test cases covering service logic
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