clowk 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 724e8bb8826e85a7ca8a5ab0a35252a173ae351914318fdf1e7337b28d0acf24
4
- data.tar.gz: '0649d40341028b61fa6bc6c5ec48f11d4426db5a3919e231b043b512df5d8676'
3
+ metadata.gz: b01072bf359496a3554b9f8d71aa01574ac36e0a3af9d3f8f1b3f70f403d9518
4
+ data.tar.gz: f50d5b860acb97711bebaed47f0ba5ab7a3cd9e22e154db84cf2d89a94e207ad
5
5
  SHA512:
6
- metadata.gz: 9d641c9a88626dbadcd07e71aa433c34526c4e5de64e4727c593ceff50d227ca29ab8cee2352f0f69912e98b10dc4e341e81aa2382be49db192937a7b7fc7a3c
7
- data.tar.gz: 6d896475327369927e31eb9ea02c40d635739b945cd01389a2431d2717030364d99fff4eac03b6f10a084a00c80dc0381b5b3d9cf55ce9f5337960b364a19152
6
+ metadata.gz: 286be44c6c4062b813f88d52c2d47c4a60210a847485a00ebad88e3c73fd4e08092704173446ba3ada396f07d46583d1d3ed5062909dae559f032cd119365d79
7
+ data.tar.gz: c8cff8c774890c14b70a8e90b16828bbf36184d00979cead290381c314f3e52d10e4d5357ff4841aa635caaf17ae843901b6bb8f449cd4947e22b746e79d8f87
data/README.md CHANGED
@@ -69,7 +69,7 @@ end
69
69
  The callback flow includes:
70
70
 
71
71
  - session-backed `state` validation
72
- - JWT verification with your instance `secret_key`
72
+ - JWT verification against Clowk's published keys (RS256), falling back to your instance `secret_key` for legacy HS256 tokens
73
73
  - internal-only redirect sanitization
74
74
  - session reset before persisting the authenticated user
75
75
  - `httponly` cookie persistence with `SameSite=Lax`
@@ -103,13 +103,56 @@ Important settings:
103
103
 
104
104
  | Setting | Purpose |
105
105
  | --- | --- |
106
- | `secret_key` | Required. Used to verify JWT signatures. |
107
- | `publishable_key` | Preferred for auth URL resolution. The gem resolves the latest instance URL from it before sign in/sign up. |
106
+ | `secret_key` | Verifies legacy HS256 tokens. Not needed to verify RS256 tokens. |
107
+ | `publishable_key` | Preferred for auth URL resolution. The gem resolves the latest instance URL from it before sign in/sign up. Also the default `audience`. |
108
108
  | `subdomain_url` | Fallback auth domain when you do not want publishable-key-based resolution. |
109
+ | `jwks_url` | Where to fetch Clowk's public keys. Defaults to `<auth domain>/.well-known/jwks.json`. |
110
+ | `audience` | Expected `aud` claim on RS256 tokens. Defaults to `publishable_key`. Set to `nil` to skip the check. |
109
111
  | `prefix_by` | Prefix used to generate helper names. Default: `:clowk`. |
110
112
  | `mount_path` | Local mount prefix used by helper path generation. Default: `/clowk`. |
111
113
  | `callback_path` | Callback route Clowk redirects back to. Default: `/clowk/oauth/callback`. |
112
114
  | `http_logger` | Optional logger used by `Clowk::Http`. |
115
+ | `session_status_cache` | Where API-only apps cache session status. Defaults to `Rails.cache`. |
116
+
117
+ ### API-only Rails apps
118
+
119
+ `Clowk::Authenticable` works in `ActionController::API` controllers with no
120
+ extra setup — no session middleware, no cookie middleware:
121
+
122
+ ```ruby
123
+ class ApplicationController < ActionController::API
124
+ include Clowk::Authenticable
125
+
126
+ before_action :authenticate_clowk!
127
+ end
128
+ ```
129
+
130
+ What changes in that mode:
131
+
132
+ - **Nothing is written back.** A bearer request produces no `Set-Cookie` and no
133
+ session write. There is no browser to hold a cookie, a mobile client ignores
134
+ it, and writing a session per request defeats the point of being stateless.
135
+ - **Failures are always 401 JSON**, never a redirect — including when the
136
+ request carries no `Accept: application/json`. A 302 to a sign-in page is
137
+ useless to an API caller.
138
+ - **Session status is cached in `session_status_cache`** instead of the Rails
139
+ session, keyed by a digest of the token. Without it, `enforce_active_session`
140
+ would cost a round trip to Clowk on every authenticated request.
141
+
142
+ ### Token verification
143
+
144
+ Tokens signed with `RS256` are verified against Clowk's public key set, fetched
145
+ from `jwks_url` and cached in memory. The `kid` header selects the key, and a
146
+ `kid` the cache has not seen triggers a single refetch — that is what makes key
147
+ rotation invisible instead of an outage.
148
+
149
+ Because every consumer trusts the same public key under RS256, the `aud` claim
150
+ is what keeps a token minted for one app from verifying in another. It defaults
151
+ to your `publishable_key`, so the check is on without extra configuration.
152
+
153
+ Tokens signed with `HS256` still verify against `secret_key`, so tokens issued
154
+ before Clowk migrated to RS256 keep working until they expire. Audience is not
155
+ enforced on that path: those tokens predate the claim.
113
156
 
114
157
  Auth URL resolution priority:
115
158
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
+ require "digest"
4
5
 
5
6
  module Clowk
6
7
  module Authenticable
@@ -107,16 +108,23 @@ module Clowk
107
108
  end
108
109
 
109
110
  def clowk_sign_out!
110
- session.delete(Clowk.config.session_key)
111
- cookies.delete(Clowk.config.cookie_key)
111
+ clowk_session_store&.delete(Clowk.config.session_key)
112
+ clowk_cookie_jar&.delete(Clowk.config.cookie_key)
112
113
 
113
114
  @clowk_current_resource = nil
114
115
  end
115
116
 
116
117
  private
117
118
 
119
+ # An API-only app has no session middleware, so there is nowhere to redirect
120
+ # a browser to and no way to come back. Redirecting there would answer a
121
+ # failed API call with a 302 to a sign-in page the caller cannot use.
122
+ def clowk_api_request?
123
+ clowk_session_store.nil? || request.format.json?
124
+ end
125
+
118
126
  def clowk_handle_unauthenticated
119
- if request.format.json?
127
+ if clowk_api_request?
120
128
  render json: {error: "Unauthorized"}, status: :unauthorized
121
129
  else
122
130
  redirect_to clowk_sign_in_path(return_to: request.fullpath)
@@ -124,13 +132,38 @@ module Clowk
124
132
  end
125
133
 
126
134
  def clowk_handle_expired_session(_session_info)
127
- if request.format.json?
135
+ if clowk_api_request?
128
136
  render json: {error: "Session expired or inactive"}, status: :unauthorized
129
137
  else
130
138
  redirect_to clowk_sign_in_path(return_to: request.fullpath)
131
139
  end
132
140
  end
133
141
 
142
+ # Probed rather than assumed: accessing `session` without the middleware
143
+ # raises, and `respond_to?` is true either way because the method is
144
+ # delegated to the request.
145
+ def clowk_session_store
146
+ return @clowk_session_store if defined?(@clowk_session_store)
147
+
148
+ @clowk_session_store = begin
149
+ store = session
150
+ store.respond_to?(:[]) ? store : nil
151
+ rescue
152
+ nil
153
+ end
154
+ end
155
+
156
+ def clowk_cookie_jar
157
+ return @clowk_cookie_jar if defined?(@clowk_cookie_jar)
158
+
159
+ @clowk_cookie_jar = begin
160
+ jar = cookies
161
+ jar.respond_to?(:[]=) ? jar : nil
162
+ rescue
163
+ nil
164
+ end
165
+ end
166
+
134
167
  def verified_request_payload
135
168
  return unless extracted_token
136
169
 
@@ -152,7 +185,10 @@ module Clowk
152
185
  end
153
186
 
154
187
  def stored_session
155
- raw_session = session[Clowk.config.session_key]
188
+ store = clowk_session_store
189
+ return if store.nil?
190
+
191
+ raw_session = store[Clowk.config.session_key]
156
192
  return unless raw_session.respond_to?(:to_h)
157
193
 
158
194
  raw_session.to_h
@@ -163,25 +199,31 @@ module Clowk
163
199
  payload&.deep_symbolize_keys
164
200
  end
165
201
 
202
+ # No-op for API-only apps. A bearer request must not come back with a
203
+ # Set-Cookie: there is no browser to hold it, the mobile client ignores it,
204
+ # and writing a session per request defeats the point of being stateless.
166
205
  def persist_clowk_session(token, payload)
167
- session[Clowk.config.session_key] = {
206
+ store = clowk_session_store
207
+ return if store.nil?
208
+
209
+ store[Clowk.config.session_key] = {
168
210
  token:,
169
211
  user: payload,
170
212
  signed_in_at: Time.now.to_i
171
213
  }
172
214
 
173
- cookies[Clowk.config.cookie_key] = {
215
+ clowk_cookie_jar&.[]=(Clowk.config.cookie_key, {
174
216
  value: token,
175
217
  httponly: true,
176
218
  same_site: :lax,
177
219
  secure: request.ssl?
178
- }
220
+ })
179
221
  end
180
222
 
181
223
  def resolve_session_status
182
- cached = stored_session&.dig("session_status") || stored_session&.dig(:session_status)
224
+ cached = clowk_read_cached_session_status
183
225
 
184
- return cached&.deep_symbolize_keys if cached && clowk_session_status_fresh?
226
+ return cached if cached
185
227
 
186
228
  resource = clowk_current_resource
187
229
 
@@ -192,16 +234,56 @@ module Clowk
192
234
  result = client.tokens.verify_with_session(token: current_token)
193
235
  status = result&.dig(:session)
194
236
 
195
- if status && stored_session
196
- session[Clowk.config.session_key] = stored_session.merge(
237
+ clowk_write_cached_session_status(status) if status
238
+
239
+ status
240
+ rescue Clowk::InvalidTokenError
241
+ nil
242
+ end
243
+
244
+ def clowk_read_cached_session_status
245
+ if clowk_session_store
246
+ cached = stored_session&.dig("session_status") || stored_session&.dig(:session_status)
247
+
248
+ return cached&.deep_symbolize_keys if cached && clowk_session_status_fresh?
249
+
250
+ return nil
251
+ end
252
+
253
+ store = clowk_status_cache
254
+ return nil unless store
255
+
256
+ store.read(clowk_status_cache_key)&.deep_symbolize_keys
257
+ end
258
+
259
+ def clowk_write_cached_session_status(status)
260
+ if clowk_session_store
261
+ clowk_session_store[Clowk.config.session_key] = stored_session.merge(
197
262
  "session_status" => status,
198
263
  "session_status_checked_at" => Time.now.to_i
199
264
  )
265
+
266
+ return
200
267
  end
201
268
 
202
- status
203
- rescue Clowk::InvalidTokenError
204
- nil
269
+ ttl = Clowk.config.session_status_ttl.to_i
270
+ store = clowk_status_cache
271
+
272
+ return unless store && ttl.positive?
273
+
274
+ store.write(clowk_status_cache_key, status, expires_in: ttl)
275
+ end
276
+
277
+ # Stateless apps have no session to hang the cached status on, so without an
278
+ # external store every authenticated request would pay a round trip to
279
+ # Clowk. Keyed by token digest — the raw token must not end up in a cache
280
+ # key that could be logged.
281
+ def clowk_status_cache
282
+ Clowk.config.session_status_cache
283
+ end
284
+
285
+ def clowk_status_cache_key
286
+ "clowk:session_status:#{Digest::SHA256.hexdigest(current_token.to_s)}"
205
287
  end
206
288
 
207
289
  # Whether the cached status may still be trusted. Timestamped alongside the
@@ -12,7 +12,9 @@ module Clowk
12
12
  attr_accessor :http_retry_attempts
13
13
  attr_accessor :http_retry_interval
14
14
  attr_accessor :http_write_timeout
15
+ attr_writer :audience
15
16
  attr_accessor :issuer
17
+ attr_accessor :jwks_url
16
18
  attr_accessor :mount_path
17
19
  attr_accessor :publishable_key
18
20
  attr_accessor :prefix_by
@@ -23,6 +25,7 @@ module Clowk
23
25
  attr_accessor :enforce_active_session
24
26
  attr_accessor :on_session_expired
25
27
  attr_accessor :session_status_ttl
28
+ attr_writer :session_status_cache
26
29
 
27
30
  def initialize
28
31
  @api_base_url = "https://api.clowk.dev/api/v1"
@@ -51,6 +54,22 @@ module Clowk
51
54
  @session_status_ttl = 300
52
55
  end
53
56
 
57
+ # Where API-only apps cache session status, since they have no Rails session
58
+ # to hang it on. Defaults to Rails.cache when Rails is loaded; set to nil to
59
+ # check with Clowk on every authenticated request.
60
+ def session_status_cache
61
+ return @session_status_cache if defined?(@session_status_cache)
62
+
63
+ @session_status_cache = (defined?(::Rails) && ::Rails.respond_to?(:cache)) ? ::Rails.cache : nil
64
+ end
65
+
66
+ # Defaults to the publishable key, which is what Clowk stamps into `aud`.
67
+ # Consumers already configure that key, so audience checking is on by
68
+ # default rather than something you have to know to switch on.
69
+ def audience
70
+ @audience.nil? ? @publishable_key : @audience
71
+ end
72
+
54
73
  def after_sign_in_path
55
74
  resolve_path(@after_sign_in_path)
56
75
  end
data/lib/clowk/engine.rb CHANGED
@@ -9,6 +9,13 @@ module Clowk
9
9
  include Clowk::Helpers::UrlHelpers
10
10
  end
11
11
 
12
+ # API-only apps never load :action_controller_base, so without this hook
13
+ # clowk_sign_in_path — reached from the unauthenticated fallback — raises
14
+ # NoMethodError instead of rendering a 401.
15
+ ActiveSupport.on_load(:action_controller_api) do
16
+ include Clowk::Helpers::UrlHelpers
17
+ end
18
+
12
19
  ActiveSupport.on_load(:action_view) do
13
20
  include Clowk::Helpers::UrlHelpers
14
21
  end
data/lib/clowk/jwks.rb ADDED
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "openssl"
6
+ require "uri"
7
+
8
+ module Clowk
9
+ # Fetches and caches Clowk's public key set.
10
+ #
11
+ # Keys are cached process-wide because verifying a token must not cost an
12
+ # HTTP round trip. A `kid` the cache has never seen forces one refetch — that
13
+ # is what makes key rotation invisible to consumers instead of an outage.
14
+ class Jwks
15
+ CACHE_TTL = 600
16
+ WELL_KNOWN_PATH = "/.well-known/jwks.json"
17
+
18
+ # Rotation should resolve in one refetch. Anything beyond that is a
19
+ # misconfigured issuer or a forged `kid`, and hammering the auth server on
20
+ # every unverifiable token turns a bad token into an outage.
21
+ REFETCH_COOLDOWN = 10
22
+
23
+ @cache_mutex = Mutex.new
24
+
25
+ class << self
26
+ def clear_cache!
27
+ @cache_mutex.synchronize do
28
+ @cache = {}
29
+ @last_refetch_at = {}
30
+ end
31
+ end
32
+
33
+ def key_for(kid, jwks_url: Clowk.config.jwks_url)
34
+ url = jwks_url || default_url
35
+
36
+ key = lookup(url, kid)
37
+ return key if key
38
+
39
+ # Cold or expired cache: this is the ordinary first fetch, not a
40
+ # rotation, so it must not spend the miss budget. Data is fresh
41
+ # afterwards, so a miss here means the kid is genuinely unknown.
42
+ return fetch_and_lookup(url, kid) unless warm?(url)
43
+
44
+ # Warm cache and an unknown kid: the set may have rotated under us.
45
+ return nil unless claim_refetch_slot!(url)
46
+
47
+ fetch_and_lookup(url, kid)
48
+ end
49
+
50
+ def default_url
51
+ base = Clowk.config.subdomain_url || Clowk::Subdomain.resolve_url!
52
+
53
+ "#{base.to_s.chomp("/")}#{WELL_KNOWN_PATH}"
54
+ rescue ConfigurationError
55
+ raise ConfigurationError, "set jwks_url, subdomain_url or publishable_key to verify RS256 tokens"
56
+ end
57
+
58
+ private
59
+
60
+ def fetch_and_lookup(url, kid)
61
+ fetch!(url)
62
+ lookup(url, kid)
63
+ end
64
+
65
+ def warm?(url)
66
+ @cache_mutex.synchronize do
67
+ entry = cache[url]
68
+
69
+ !entry.nil? && entry[:expires_at] > Time.now
70
+ end
71
+ end
72
+
73
+ def lookup(url, kid)
74
+ @cache_mutex.synchronize do
75
+ entry = cache[url]
76
+
77
+ next nil unless entry
78
+ next nil if entry[:expires_at] <= Time.now
79
+
80
+ entry[:keys][kid]
81
+ end
82
+ end
83
+
84
+ # Throttles only the refetches a cache miss triggers, not ordinary TTL
85
+ # refreshes. A rotation is picked up on the very next token; a storm of
86
+ # forged kids costs at most one fetch per cooldown window.
87
+ def claim_refetch_slot!(url)
88
+ @cache_mutex.synchronize do
89
+ last = last_refetch_at[url]
90
+
91
+ next false if last && Time.now - last < REFETCH_COOLDOWN
92
+
93
+ last_refetch_at[url] = Time.now
94
+ true
95
+ end
96
+ end
97
+
98
+ def fetch!(url)
99
+ uri = URI.parse(url)
100
+ base = uri.dup
101
+ base.path = ""
102
+ base.query = nil
103
+ base.fragment = nil
104
+
105
+ response = Http.get(base_url: base.to_s, path: uri.path, logger: Clowk.config.http_logger)
106
+
107
+ raise InvalidTokenError, "could not fetch JWKS from #{url}" unless response.success?
108
+
109
+ keys = parse(response.body)
110
+
111
+ @cache_mutex.synchronize do
112
+ cache[url] = {keys: keys, expires_at: Time.now + CACHE_TTL}
113
+ end
114
+
115
+ keys
116
+ end
117
+
118
+ def parse(body)
119
+ payload = body.is_a?(Hash) ? body : JSON.parse(body.to_s)
120
+ entries = payload["keys"] || payload[:keys] || []
121
+
122
+ entries.each_with_object({}) do |jwk, acc|
123
+ jwk = jwk.transform_keys(&:to_s)
124
+ next unless jwk["kty"] == "RSA" && jwk["kid"]
125
+
126
+ acc[jwk["kid"]] = to_rsa(jwk)
127
+ end
128
+ rescue JSON::ParserError => e
129
+ raise InvalidTokenError, "malformed JWKS document: #{e.message}"
130
+ end
131
+
132
+ # Built through DER rather than assigning n/e onto a blank key: OpenSSL 3
133
+ # made key objects immutable, so `set_key` no longer exists.
134
+ def to_rsa(jwk)
135
+ sequence = OpenSSL::ASN1::Sequence([
136
+ OpenSSL::ASN1::Integer(decode_bn(jwk["n"])),
137
+ OpenSSL::ASN1::Integer(decode_bn(jwk["e"]))
138
+ ])
139
+
140
+ OpenSSL::PKey::RSA.new(sequence.to_der)
141
+ end
142
+
143
+ def decode_bn(value)
144
+ OpenSSL::BN.new(Base64.urlsafe_decode64(pad(value.to_s)), 2)
145
+ end
146
+
147
+ # JWKS values are unpadded base64url; Ruby's decoder wants padding.
148
+ def pad(value)
149
+ remainder = value.length % 4
150
+
151
+ remainder.zero? ? value : value + ("=" * (4 - remainder))
152
+ end
153
+
154
+ def cache
155
+ @cache ||= {}
156
+ end
157
+
158
+ def last_refetch_at
159
+ @last_refetch_at ||= {}
160
+ end
161
+ end
162
+ end
163
+ end
@@ -4,24 +4,87 @@ require "jwt"
4
4
 
5
5
  module Clowk
6
6
  class JwtVerifier
7
- ALGORITHM = "HS256"
7
+ LEGACY_ALGORITHM = "HS256"
8
+ ASYMMETRIC_ALGORITHM = "RS256"
8
9
 
9
- def initialize(secret_key: Clowk.config.secret_key, issuer: Clowk.config.issuer)
10
+ # Kept because it is part of the gem's public surface. Prefer
11
+ # LEGACY_ALGORITHM — this no longer describes what the verifier accepts.
12
+ ALGORITHM = LEGACY_ALGORITHM
13
+
14
+ def initialize(secret_key: Clowk.config.secret_key, issuer: Clowk.config.issuer,
15
+ audience: Clowk.config.audience, jwks_url: Clowk.config.jwks_url)
10
16
  @secret_key = secret_key
11
17
  @issuer = issuer
18
+ @audience = audience
19
+ @jwks_url = jwks_url
12
20
  end
13
21
 
14
22
  def verify(token)
23
+ payload, = asymmetric?(token) ? verify_asymmetric(token) : verify_legacy(token)
24
+ payload.deep_symbolize_keys
25
+ rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature,
26
+ JWT::InvalidIssuerError, JWT::InvalidAudError => e
27
+ raise InvalidTokenError, e.message
28
+ end
29
+
30
+ private
31
+
32
+ def asymmetric?(token)
33
+ header(token)["alg"] == ASYMMETRIC_ALGORITHM
34
+ rescue JWT::DecodeError
35
+ false
36
+ end
37
+
38
+ def header(token)
39
+ JWT.decode(token, nil, false).last
40
+ end
41
+
42
+ # Tokens signed with the shared instance secret. Kept so tokens issued
43
+ # before the RS256 migration keep working until they expire.
44
+ def verify_legacy(token)
15
45
  raise ConfigurationError, "missing Clowk secret_key" if @secret_key.to_s.empty?
16
46
 
17
- options = {algorithm: ALGORITHM}
18
- options[:iss] = @issuer if @issuer
19
- options[:verify_iss] = @issuer.present?
47
+ JWT.decode(token, @secret_key, true, base_options.merge(algorithm: LEGACY_ALGORITHM))
48
+ end
20
49
 
21
- payload, = JWT.decode(token, @secret_key, true, options)
22
- payload.deep_symbolize_keys
23
- rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature, JWT::InvalidIssuerError => e
24
- raise InvalidTokenError, e.message
50
+ def verify_asymmetric(token)
51
+ kid = header(token)["kid"]
52
+
53
+ raise InvalidTokenError, "token is missing kid header" if kid.to_s.empty?
54
+
55
+ key = Jwks.key_for(kid, jwks_url: @jwks_url)
56
+
57
+ raise InvalidTokenError, "unknown signing key: #{kid}" unless key
58
+
59
+ JWT.decode(token, key, true, asymmetric_options)
60
+ end
61
+
62
+ def base_options
63
+ opts = {}
64
+
65
+ if @issuer
66
+ opts[:iss] = @issuer
67
+ opts[:verify_iss] = true
68
+ end
69
+
70
+ opts
71
+ end
72
+
73
+ # Audience is enforced only here, and deliberately so. Under RS256 every
74
+ # consumer trusts the same public key, so without `aud` a token minted for
75
+ # another app would verify cleanly — the shared secret used to prevent that
76
+ # by accident. HS256 tokens are already scoped by the per-instance secret,
77
+ # and the ones issued before this claim existed carry no `aud` at all, so
78
+ # enforcing it there would reject valid tokens during the migration window.
79
+ def asymmetric_options
80
+ opts = base_options.merge(algorithm: ASYMMETRIC_ALGORITHM)
81
+
82
+ if @audience
83
+ opts[:aud] = @audience
84
+ opts[:verify_aud] = true
85
+ end
86
+
87
+ opts
25
88
  end
26
89
  end
27
90
  end
data/lib/clowk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clowk
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/clowk.rb CHANGED
@@ -48,6 +48,7 @@ require_relative "clowk/sdk/token"
48
48
  require_relative "clowk/sdk/session_config"
49
49
  require_relative "clowk/sdk/client"
50
50
  require_relative "clowk/subdomain"
51
+ require_relative "clowk/jwks"
51
52
  require_relative "clowk/jwt_verifier"
52
53
  require_relative "clowk/helpers/url_helpers"
53
54
  require_relative "clowk/middleware/token_extractor"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clowk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clowk
@@ -115,6 +115,7 @@ files:
115
115
  - lib/clowk/http/response.rb
116
116
  - lib/clowk/http/retry_middleware.rb
117
117
  - lib/clowk/http/timeout_middleware.rb
118
+ - lib/clowk/jwks.rb
118
119
  - lib/clowk/jwt_verifier.rb
119
120
  - lib/clowk/middleware/token_extractor.rb
120
121
  - lib/clowk/sdk/client.rb