Module: WhopSDK::Helpers::VerifyUserToken

Defined in:
lib/whop_sdk/helpers/verify_user_token.rb

Overview

Verifies Whop-issued x-whop-user-token JWTs. By default, fetches the public signing keys from Whop’s canonical JWKS endpoint and caches them at module scope (TTL-bounded with a cooldown on refetch). The behavior mirrors the TypeScript SDK’s ‘createRemoteJWKSet` path so that upgrading to a key rotation doesn’t require a gem release.

Defined Under Namespace

Classes: RemoteJwks, UserTokenPayload

Constant Summary collapse

USER_TOKEN_HEADER_NAME =
"x-whop-user-token"
DEFAULT_JWKS_URL =
"https://api.whop.com/.well-known/jwks.json"
TOKEN_ISSUER =
"urn:whopcom:exp-proxy"
TOKEN_ALGORITHM =
"ES256"
JWKS_CACHE_MAX_AGE_SECONDS =

12h freshness window before a proactive refresh; 30s cooldown between refetches when a kid lookup misses. Matches the TS SDK (‘cacheMaxAge: 12 * 60 * 60 * 1000, cooldownDuration: 30_000`).

12 * 60 * 60
JWKS_COOLDOWN_SECONDS =
30

Class Method Summary collapse

Class Method Details

.get_user_token(token_or_headers, header_name: nil) ⇒ String?

Extracts the user token from various input types.

Parameters:

  • token_or_headers (String, Hash, nil)

    token string or headers hash

  • header_name (String, nil) (defaults to: nil)

    header name (defaults to x-whop-user-token)

Returns:

  • (String, nil)


120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 120

def self.get_user_token(token_or_headers, header_name: nil)
  header_name ||= USER_TOKEN_HEADER_NAME

  case token_or_headers
  when String
    token_or_headers
  when Hash
    token_or_headers[header_name] ||
      token_or_headers[header_name.downcase] ||
      token_or_headers[header_name.upcase]
  end
end

.import_static_key(public_key) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



222
223
224
225
226
227
228
229
230
231
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 222

def self.import_static_key(public_key)
  stripped = public_key.to_s.strip
  if stripped.start_with?("-----BEGIN")
    OpenSSL::PKey::EC.new(stripped)
  else
    JWT::JWK.new(JSON.parse(stripped)).verify_key
  end
rescue JSON::ParserError, OpenSSL::PKey::ECError => e
  raise StandardError, "Invalid public key provided to verifyUserToken: #{e.message}"
end

.remote_jwks_for(url) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



100
101
102
103
104
105
106
107
108
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 100

def self.remote_jwks_for(url)
  @jwks_cache_mutex.synchronize do
    @jwks_cache[url] ||= RemoteJwks.new(
      url,
      cache_max_age_seconds: JWKS_CACHE_MAX_AGE_SECONDS,
      cooldown_seconds: JWKS_COOLDOWN_SECONDS
    )
  end
end

.reset_jwks_cache!Object



111
112
113
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 111

def self.reset_jwks_cache!
  @jwks_cache_mutex.synchronize { @jwks_cache.clear }
end

.verify_user_token!(token_or_headers, app_id: nil, public_key: nil, jwks_url: nil, header_name: nil) ⇒ UserTokenPayload

Verifies a Whop user token.

By default fetches the public signing keys from ‘jwks_url` (or the canonical Whop endpoint) and caches them at module scope. Pass `public_key` to verify against a static key instead — useful for self-hosted / test setups where you know the exact key.

Parameters:

  • token_or_headers (String, Hash, nil)
  • app_id (String, nil) (defaults to: nil)

    Required; when set, the aud claim must match.

  • public_key (String, nil) (defaults to: nil)

    PEM-encoded EC public key or JWK JSON. When set, skips remote JWKS fetching entirely.

  • jwks_url (String, nil) (defaults to: nil)

    Override the JWKS endpoint URL. Defaults to DEFAULT_JWKS_URL.

  • header_name (String, nil) (defaults to: nil)

    Header to read the token from.

Returns:

Raises:

  • (StandardError)

    on validation failure.



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 149

def self.verify_user_token!(
  token_or_headers,
  app_id: nil,
  public_key: nil,
  jwks_url: nil,
  header_name: nil
)
  token_string = get_user_token(token_or_headers, header_name: header_name)

  if token_string.nil? || token_string.empty?
    raise StandardError, <<~ERROR
      Whop user token not found.
      If you are the app developer, ensure you are developing in the whop.com iframe and have the dev proxy enabled.
    ERROR
  end

  payload = if public_key
    verify_with_static_key(token_string, public_key: public_key)
  else
    verify_with_remote_jwks(token_string, jwks_url: jwks_url || DEFAULT_JWKS_URL)
  end

  unless payload["sub"] && payload["aud"] && !payload["aud"].is_a?(Array)
    raise StandardError, "Invalid user token provided to verifyUserToken"
  end

  if app_id && payload["aud"] != app_id
    raise StandardError, "Invalid app id provided to verifyUserToken"
  end

  UserTokenPayload.new(user_id: payload["sub"], app_id: payload["aud"])
end

.verify_with_remote_jwks(token_string, jwks_url:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 197

def self.verify_with_remote_jwks(token_string, jwks_url:)
  remote = remote_jwks_for(jwks_url)

  # ruby-jwt calls the loader twice when the token's kid isn't found
  # in the current set — once normally, then again with
  # invalidate: true. The loader uses that signal to force a
  # cooldown-guarded refresh, mirroring jose's remote JWKS behavior.
  jwks_loader = ->(opts) { remote.jwk_set(force_refresh: opts[:invalidate]) }

  decoded_payload, = JWT.decode(
    token_string,
    nil,
    true,
    algorithms: [TOKEN_ALGORITHM],
    iss: TOKEN_ISSUER,
    verify_iss: true,
    jwks: jwks_loader,
    # Legacy tokens (pre-kid rollout) have no kid header. Let
    # ruby-jwt fall back to the first key in the set for those.
    allow_nil_kid: true
  )
  decoded_payload
end

.verify_with_static_key(token_string, public_key:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 183

def self.verify_with_static_key(token_string, public_key:)
  key = import_static_key(public_key)
  decoded_payload, = JWT.decode(
    token_string,
    key,
    true,
    algorithm: TOKEN_ALGORITHM,
    iss: TOKEN_ISSUER,
    verify_iss: true
  )
  decoded_payload
end