# frozen_string_literal: true require "faraday" require "logger" require "net/http" require "json" require "jwt" require "concurrent-ruby" require_relative "resources/allowlist_identifiers" require_relative "resources/allowlist" require_relative "resources/clients" require_relative "resources/email_addresses" require_relative "resources/emails" require_relative "resources/organizations" require_relative "resources/phone_numbers" require_relative "resources/sessions" require_relative "resources/sms_messages" require_relative "resources/users" require_relative "resources/users" require_relative "resources/jwks" require_relative "errors" require_relative "jwks_cache" module Clerk class SDK DEFAULT_HEADERS = { "User-Agent" => "Clerk/#{Clerk::VERSION}; Faraday/#{Faraday::VERSION}; Ruby/#{RUBY_VERSION}", "X-Clerk-SDK" => "ruby/#{Clerk::VERSION}" } # How often (in seconds) should JWKs be refreshed JWKS_CACHE_LIFETIME = 3600 # 1 hour @@jwks_cache = JWKSCache.new(JWKS_CACHE_LIFETIME) def self.jwks_cache @@jwks_cache end def initialize(api_key: nil, base_url: nil, logger: nil, ssl_verify: true, connection: nil) if connection # Inject a Faraday::Connection for testing or full control over Faraday @conn = connection return else base_url = base_url || Clerk.configuration.base_url base_uri = if !base_url.end_with?("/") URI("#{base_url}/") else URI(base_url) end api_key ||= if Faraday::VERSION.to_i < 2 Clerk.configuration.api_key elsif Clerk.configuration.api_key.nil? -> { raise ArgumentError, "Clerk secret key is not set" } end logger = logger || Clerk.configuration.logger @conn = Faraday.new( url: base_uri, headers: DEFAULT_HEADERS, ssl: {verify: ssl_verify} ) do |f| f.request :url_encoded f.request :authorization, "Bearer", api_key if logger f.response :logger, logger do |l| l.filter(/(Authorization: "Bearer) (\w+)/, '\1 [SECRET]') end end end end end def request(method, path, query: [], body: nil, timeout: nil) response = case method when :get @conn.get(path, query) do |req| req.options.timeout = timeout if timeout end when :post @conn.post(path, body) do |req| req.body = body.to_json req.headers[:content_type] = "application/json" req.options.timeout = timeout if timeout end when :patch @conn.patch(path, body) do |req| req.body = body.to_json req.headers[:content_type] = "application/json" req.options.timeout = timeout if timeout end when :delete @conn.delete(path) do |req| req.options.timeout = timeout if timeout end end body = if response["Content-Type"] == "application/json" JSON.parse(response.body) else response.body end if response.success? body else klass = case body.dig("errors", 0, "code") when "cookie_invalid", "client_not_found", "resource_not_found" Errors::Authentication else Errors::Fatal end raise klass.new(body, status: response.status) end end def allowlist_identifiers Resources::AllowlistIdentifiers.new(self) end def allowlist Resources::Allowlist.new(self) end def clients Resources::Clients.new(self) end def email_addresses Resources::EmailAddresses.new(self) end def emails Resources::Emails.new(self) end def organizations Resources::Organizations.new(self) end def phone_numbers Resources::PhoneNumbers.new(self) end def sessions Resources::Sessions.new(self) end def sms_messages Resources::SMSMessages.new(self) end def users Resources::Users.new(self) end def jwks Resources::JWKS.new(self) end def interstitial(refresh=false) request(:get, "internal/interstitial") end # Returns the decoded JWT payload without verifying if the signature is # valid. # # WARNING: This will not verify whether the signature is valid. You # should not use this for untrusted messages! You most likely want to use # verify_token. def decode_token(token) JWT.decode(token, nil, false).first end # Decode the JWT and verify it's valid (verify claims, signature etc.) using # the provided algorithms. # # JWKS are cached for JWKS_CACHE_LIFETIME seconds, in order to avoid # unecessary roundtrips. In order to invalidate the cache, pass # `force_refresh_jwks: true`. # # A timeout for the request to the JWKs endpoint can be set with the # `timeout` argument. def verify_token(token, force_refresh_jwks: false, algorithms: ['RS256'], timeout: 5) jwk_loader = ->(options) do # JWT.decode requires that the 'keys' key in the Hash is a symbol (as # opposed to a string which our SDK returns by default) { keys: SDK.jwks_cache.fetch(self, kid_not_found: (options[:invalidate] || options[:kid_not_found]), force_refresh: force_refresh_jwks) } end JWT.decode(token, nil, true, algorithms: algorithms, jwks: jwk_loader).first end end end