Class: Clerk::SDK

Inherits:
OpenAPIClient
  • Object
show all
Defined in:
lib/clerk/sdk.rb

Constant Summary collapse

JWKS_CACHE_LIFETIME =

How often (in seconds) should JWKs be refreshed

3600
@@jwks_caches =

One JWKS cache per Clerk instance. A single process-wide cache would let a token minted by one instance verify against another, since the keys it holds are whichever instance refreshed last and no issuer check runs. The cache still outlives individual SDK objects, because authenticate_request builds a fresh SDK per request. rubocop:disable Style/ClassVars

{}
@@jwks_caches_mutex =
Mutex.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client: nil, retry_config: nil, timeout_ms: nil, secret_key: nil, security_source: nil, server_idx: nil, server_url: nil, url_params: nil) ⇒ SDK

Returns a new instance of SDK.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/clerk/sdk.rb', line 42

def initialize(client: nil, retry_config: nil, timeout_ms: nil, secret_key: nil, security_source: nil, server_idx: nil, server_url: nil, url_params: nil)
  secret_key ||= Clerk.configuration.secret_key
  @jwks_cache_scope = Digest::SHA256.hexdigest(
    [server_url, server_idx, secret_key].join("\0")
  )
  super(
    client: client,
    retry_config: retry_config,
    timeout_ms: timeout_ms,
    bearer_auth: secret_key,
    security_source: security_source,
    server_idx: server_idx,
    server_url: server_url,
    url_params: url_params
  )
end

Instance Attribute Details

#jwks_cache_scopeObject (readonly)

Opaque identifier for the Clerk instance this SDK talks to. The secret key is hashed so it is never retained as a cache key.



40
41
42
# File 'lib/clerk/sdk.rb', line 40

def jwks_cache_scope
  @jwks_cache_scope
end

Class Method Details

.jwks_cache(scope) ⇒ Object

Returns the JWKS cache for a single Clerk instance. scope must identify that instance; see #jwks_cache_scope.



32
33
34
35
36
# File 'lib/clerk/sdk.rb', line 32

def self.jwks_cache(scope)
  @@jwks_caches_mutex.synchronize do
    @@jwks_caches[scope] ||= JWKSCache.new(JWKS_CACHE_LIFETIME)
  end
end

Instance Method Details

#decode_token(token) ⇒ Object

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.



63
64
65
# File 'lib/clerk/sdk.rb', line 63

def decode_token(token)
  JWT.decode(token, nil, false).first
end

#verify_token(token, force_refresh_jwks: false, algorithms: ['RS256'], timeout: 5) ⇒ Object

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.



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/clerk/sdk.rb', line 73

def verify_token(token, force_refresh_jwks: false, algorithms: ['RS256'], timeout: 5)
  jwk_loader = lambda do |options|
    # 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(@jwks_cache_scope).fetch(self, kid_not_found: options[:invalidate] || options[:kid_not_found], force_refresh: force_refresh_jwks)}
  end

  begin
    claims = JWT.decode(token, nil, true, algorithms: algorithms, exp_leeway: timeout, jwks: jwk_loader).first
  rescue JWT::ExpiredSignature => e
    raise e
  rescue JWT::InvalidIatError => e
    raise e
  rescue JWT::DecodeError => e
    raise e
  rescue StandardError => e
    raise e
  end

  # orgs
  if claims['v'].nil? || claims['v'] == 1
    claims['v'] = 1
  elsif claims['v'] == 2 && claims['o']
    claims['org_id']          = claims['o'].fetch('id', nil)
    claims['org_slug']        = claims['o'].fetch('slg', nil)
    claims['org_role']        = "org:#{claims['o'].fetch('rol', nil)}"

    org_permissions = compute_org_permissions_from_v2_token(claims)
    claims['org_permissions'] = org_permissions if org_permissions.any?
    claims.delete('o')
    claims.delete('fea')
  end

  claims
end