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)


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

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.



216
217
218
219
220
221
222
223
224
225
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 216

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.



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

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



109
110
111
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 109

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.



147
148
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
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 147

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

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

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

  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.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 191

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.



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/whop_sdk/helpers/verify_user_token.rb', line 177

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