firebase_id_token 3.0.0 → 4.0.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: b0bb2d6c4da0bacd15e8a551b29b48f3ee99018697334a437f11c5ee03bd2a11
4
- data.tar.gz: 936cf705c0130dc0927e0b51811f3cba131970990c7ee5e480fe1705abde2e6f
3
+ metadata.gz: a7737e97a521a870ff07e83c63f2dbf0d72e89ba85858f42ae9443762518a15a
4
+ data.tar.gz: 20f27c0f72411d19d6f0efb29fb48528a31c5b6a9ccee7d42300ff61f08833c9
5
5
  SHA512:
6
- metadata.gz: 62bf3a86a503fba14deac924a837b739c5cba3e7a93ad560a9f9f2b320edf704d7df33897b7422ebffbe47698252c49e00ab72cad3e08da1fb6e43066c0345cd
7
- data.tar.gz: ad1a1ed121307e5b79d3943d9b37624bacf8b42f8b171642de6052d50bf313d7ec3f777f99d93f7f31233c45d1bb2365ba10fff3bede139458b9528d25c1a219
6
+ metadata.gz: 2e120f07c5e5017932ab02dcd7955bc3ee292ed360f6da45f56f739394488aa8ae41bab42ab30dbd1f4dd7f51e3dfeed6c3d3a1acd00925656e87be8253a3cda
7
+ data.tar.gz: a84b333fc8a2a9ad2cec07300ba4ad46a72b9685cebb9cfd0dcb7e2651a123cfed5732487851b76e0a0f8a2ef897989e417418eecf0d38375b287e582a0dd0c9
@@ -3,6 +3,7 @@ on:
3
3
  push:
4
4
  branches:
5
5
  - master
6
+ pull_request:
6
7
 
7
8
  jobs:
8
9
  test:
@@ -25,10 +26,3 @@ jobs:
25
26
  run: bundle install
26
27
  - name: Run tests
27
28
  run: bundle exec rspec
28
- - name: Publish code coverage
29
- uses: paambaati/codeclimate-action@v4.0.0
30
- env:
31
- CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
32
- with:
33
- debug: true
34
- coverageLocations: ${{github.workspace}}/coverage/coverage.json:simplecov
data/CHANGELOG.md CHANGED
@@ -6,7 +6,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- Nothing.
9
+ ## [4.0.0] - 2026-07-28
10
+
11
+ ### Added
12
+ - Support for verifying Firebase Session Cookies with
13
+ `FirebaseIdToken::Signature.verify(cookie, type: :session_cookie)`. Session
14
+ Cookie certificates are downloaded from their own Google API URL and cached
15
+ apart from the ID Token ones.
16
+ [PR #46](https://github.com/fschuindt/firebase_id_token/pull/46).
17
+ - Support for any `ActiveSupport::Cache` store through the new
18
+ `config.cache_store` configuration, with lazy certificate download on cache
19
+ miss, removing the need for Redis and scheduled certificate requests.
20
+ [PR #43](https://github.com/fschuindt/firebase_id_token/pull/43), closes
21
+ [issue #6](https://github.com/fschuindt/firebase_id_token/issues/6). The
22
+ legacy `config.redis` configuration keeps working as before.
23
+ - A warning on `README.md` that `FirebaseIdToken.test!` must never be called
24
+ outside of a test suite, as the fixture private key is public in this
25
+ repository.
26
+ - [Dev] `ostruct` as a development dependency, as it's no longer a default
27
+ gem in Ruby >= 3.5 and Pry requires it when running the specs.
28
+
29
+ ### Fixed
30
+ - `Signature.verify` raised `NoMethodError` when a correctly signed token had
31
+ no `sub` claim, even with `raise_error: false`. It now returns `nil` as
32
+ documented.
33
+
34
+ ### Changed
35
+ - Loosened the HTTParty dependency constraint to `>= 0.21, < 1.0`, allowing
36
+ newer releases. [Issue #44](https://github.com/fschuindt/firebase_id_token/issues/44).
37
+ - CI now also runs on pull requests.
38
+
39
+ ### Removed
40
+ - The Code Climate coverage publishing step and badges, as the service and
41
+ its GitHub Action are defunct. SimpleCov still reports coverage on every
42
+ test run.
10
43
 
11
44
  ## [3.0.0] - 2023-04-11
12
45
 
@@ -20,6 +53,7 @@ Nothing.
20
53
  - SimpleCov JSON formatter and `json` as dependency.
21
54
 
22
55
  ### Changed
56
+ - Redis is no longer required. Any ActiveSupport cache will now work.
23
57
  - It won't default to `Redis.new` anymore. You must now provide Redis details during configuration. [Details here](https://github.com/fschuindt/firebase_id_token/issues/30).
24
58
  - Upgraded Redis to 5.0.6.
25
59
  - Upgraded Redis Namespace to 1.10.
@@ -152,6 +186,7 @@ Nothing.
152
186
  ## [0.1.0] - 2017-04-23
153
187
  *Version removed.*
154
188
 
189
+ [4.0.0]: https://github.com/fschuindt/firebase_id_token/compare/3.0.0...4.0.0
155
190
  [3.0.0]: https://github.com/fschuindt/firebase_id_token/compare/2.5.2...3.0.0
156
191
  [2.5.2]: https://github.com/fschuindt/firebase_id_token/compare/2.5.1...2.5.2
157
192
  [2.5.1]: https://github.com/fschuindt/firebase_id_token/compare/2.5.0...2.5.1
data/Gemfile CHANGED
@@ -2,3 +2,10 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in firebase_id_token.gemspec
4
4
  gemspec
5
+
6
+ # connection_pool 3.x is incompatible with ActiveSupport 7.x's
7
+ # RedisCacheStore (positional options hash vs keyword arguments).
8
+ gem 'connection_pool', '< 3.0'
9
+
10
+ # No longer a default gem in Ruby >= 3.5; needed by pry in the specs.
11
+ gem 'ostruct'
data/README.md CHANGED
@@ -1,26 +1,18 @@
1
- # Ruby Firebase ID Token verifier (pre-release)
1
+ # Ruby Firebase ID Token verifier
2
2
 
3
- [![Actions Status](https://github.com/fschuindt/firebase_id_token/workflows/Test/badge.svg?branch=master)](https://github.com/fschuindt/firebase_id_token/actions?query=workflow%3ATest)
4
- [![Test Coverage](https://codeclimate.com/github/fschuindt/firebase_id_token/badges/coverage.svg)](https://codeclimate.com/github/fschuindt/firebase_id_token/coverage)
5
- [![Code Climate](https://codeclimate.com/github/fschuindt/firebase_id_token/badges/gpa.svg)](https://codeclimate.com/github/fschuindt/firebase_id_token)
6
- [![Issue Count](https://codeclimate.com/github/fschuindt/firebase_id_token/badges/issue_count.svg)](https://codeclimate.com/github/fschuindt/firebase_id_token)
7
- [![Inline docs](http://inch-ci.org/github/fschuindt/firebase_id_token.svg?branch=master)](http://inch-ci.org/github/fschuindt/firebase_id_token)
3
+ [![Actions Status](https://github.com/fschuindt/firebase_id_token/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/fschuindt/firebase_id_token/actions/workflows/test.yml)
8
4
 
9
- A Ruby gem to verify the signature of Firebase ID Tokens (JWT). It uses Redis to store Google's x509 certificates and manage their expiration time, so you don't need to request Google's API in every execution and can access it as fast as reading from memory.
5
+ A Ruby gem to verify the signature of Firebase ID Tokens (JWT). It uses ActiveSupport::Cache to store Google's x509 certificates and manage their expiration time, so you don't need to request Google's API in every execution and can access it as fast as reading from memory.
10
6
 
11
7
  It also checks the JWT payload parameters as recommended [here](https://firebase.google.com/docs/auth/admin/verify-id-tokens) by Firebase official documentation.
12
8
 
13
- Feel free to open any issue or to [contact me](https://fschuindt.github.io/blog/about/) directly.
9
+ Feel free to open any issue or to [contact me](https://fschuindt.github.io/blog/about/) directly.
14
10
  Any contribution is welcome.
15
11
 
16
12
  ## Docs
17
13
 
18
14
  + http://www.rubydoc.info/gems/firebase_id_token
19
15
 
20
- ## Requirements
21
-
22
- + Redis
23
-
24
16
  ## Installing
25
17
 
26
18
  ```
@@ -29,7 +21,7 @@ gem install firebase_id_token
29
21
 
30
22
  or in your Gemfile
31
23
  ```
32
- gem 'firebase_id_token', '~> 3.0.0'
24
+ gem 'firebase_id_token', '~> 4.0.0'
33
25
  ```
34
26
  then
35
27
  ```
@@ -42,13 +34,27 @@ It's needed to set up your Firebase Project ID.
42
34
 
43
35
  If you are using Rails, this should probably go into `config/initializers/firebase_id_token.rb`.
44
36
  ```ruby
37
+ FirebaseIdToken.configure do |config|
38
+ config.cache_store = ActiveSupport::Cache::RedisCacheStore.new
39
+ config.project_ids = ['your-firebase-project-id']
40
+ end
41
+ ```
42
+
43
+ You can use the old method of configuration as well. If you use this method, you'll have to proactively
44
+ download certificates (see [Downloading Certificates](#downloading-certificates) below)
45
+ ```ruby
45
46
  FirebaseIdToken.configure do |config|
46
47
  config.redis = Redis.new
47
48
  config.project_ids = ['your-firebase-project-id']
48
49
  end
49
50
  ```
50
51
 
51
- - `redis` with a `Redis` instance must be supplied. You can configure your Redis details here. Example: `Redis.new(host: '10.0.1.1', port: 6380, db: 15)`.
52
+ - A cache store instance inheriting from [ActiveSupport::Cache::Store](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html) must be supplied.
53
+ - Examples:
54
+ - `ActiveSupport::Cache::RedisCacheStore.new(Redis.new(host: '10.0.1.1', port: 6380, db: 15))`
55
+ - `ActiveSupport::Cache::MemoryStore.new(namespace: "firebase_auth")`
56
+ - `ActiveSupport::Cache::FileStore.new("cache", namespace: "firebase_auth")`
57
+ - `Rails.cache`
52
58
  - `project_ids` must be an Array.
53
59
 
54
60
  *If you want to verify signatures from more than one Firebase project, just add more Project IDs to the list.*
@@ -67,22 +73,22 @@ To do it, simply:
67
73
  FirebaseIdToken::Certificates.request
68
74
  ```
69
75
 
70
- It will download the certificates and save it in Redis, but only if the Redis certificates database is empty. To force download and override of the Redis database, use:
76
+ It will download the certificates and save it in cache, but only if the cache certificates database is empty. To force download and override of the cache entries, use:
71
77
  ```ruby
72
78
  FirebaseIdToken::Certificates.request!
73
79
  ```
74
80
 
75
- Google give us information about the certificates' expiration time, it's used to set a Redis TTL (Time-To-Live) when saving it. By doing so, the certificates will be automatically deleted after its expiration.
81
+ Google give us information about the certificates' expiration time, it's used to set a cache TTL (Time-To-Live) when saving it. By doing so, the certificates will be automatically deleted after its expiration.
76
82
 
77
83
  #### Certificates Info
78
84
 
79
- Checks the presence of certificates in the Redis database.
85
+ Checks the presence of certificates in the cache.
80
86
  ```ruby
81
87
  FirebaseIdToken::Certificates.present?
82
88
  => true
83
89
  ```
84
90
 
85
- How many seconds until the certificate's expiration.
91
+ How many seconds until the certificate's expiration. _NOTE_: Currently only functional when using Redis
86
92
  ```ruby
87
93
  FirebaseIdToken::Certificates.ttl
88
94
  => 22352
@@ -101,6 +107,8 @@ FirebaseIdToken::Certificates.find('ec8f292sd30224afac5c55540df66d1f999d')
101
107
  ```
102
108
 
103
109
  #### Downloading in Rails
110
+ If you pass in the `cache_store` configuration option (see [configuration](#configuration)), the certificates will be
111
+ requested at runtime when needed and you can ignore this section.
104
112
 
105
113
  If you are using Rails, it's clever to download certificates in a cron task, you can use [whenever](https://github.com/javan/whenever).
106
114
 
@@ -112,12 +120,12 @@ Create your task in `lib/tasks/firebase.rake`:
112
120
  ```ruby
113
121
  namespace :firebase do
114
122
  namespace :certificates do
115
- desc "Request Google's x509 certificates when Redis is empty"
123
+ desc "Request Google's x509 certificates when the cache is empty"
116
124
  task request: :environment do
117
125
  FirebaseIdToken::Certificates.request
118
126
  end
119
127
 
120
- desc "Request Google's x509 certificates and override Redis"
128
+ desc "Request Google's x509 certificates and override the cache"
121
129
  task force_request: :environment do
122
130
  FirebaseIdToken::Certificates.request!
123
131
  end
@@ -144,7 +152,7 @@ When developing, you should just run the task:
144
152
  $ rake firebase:certificates:request
145
153
  ```
146
154
 
147
- *You need Redis to be running.*
155
+ *You need Redis to be running if you're using RedisCacheStore*
148
156
 
149
157
  ### Verifying Tokens
150
158
 
@@ -164,6 +172,21 @@ FirebaseIdToken::Signature.verify('aaaaaa')
164
172
  => nil
165
173
  ```
166
174
 
175
+ #### Verifying Session Cookies
176
+
177
+ [Firebase Session Cookies](https://firebase.google.com/docs/auth/admin/manage-cookies) are issued by `https://session.firebase.google.com` and signed by a different certificate set than ID Tokens. Pass `type: :session_cookie` to verify one:
178
+
179
+ ```ruby
180
+ FirebaseIdToken::Signature.verify(session_cookie, type: :session_cookie)
181
+ => {"iss"=>"https://session.firebase.google.com/firebase-id-token", "name"=>"Bob Test", [...]}
182
+ ```
183
+
184
+ When using the `cache_store` configuration, the Session Cookie certificates are downloaded automatically on the first verification, just like the ID Token ones. If you are on the legacy Redis configuration and download certificates with a cron task, you also need to request the Session Cookie certificates:
185
+
186
+ ```ruby
187
+ FirebaseIdToken::Certificates.request(source: :session_cookie)
188
+ ```
189
+
167
190
  #### WARNING!
168
191
 
169
192
  ##### Expired tokens can point to long gone certificates
@@ -180,7 +203,7 @@ More details [here](https://github.com/fschuindt/firebase_id_token/issues/29).
180
203
 
181
204
  ##### Trying to verify tokens without downloaded certificates will raise an error
182
205
 
183
- If you try to verify a signature without any certificates in Redis database, it will raise a `FirebaseIdToken::Exceptions::NoCertificatesError`.
206
+ If you try to verify a signature without any certificates in the cache, it will raise a `FirebaseIdToken::Exceptions::NoCertificatesError`.
184
207
 
185
208
  ##### "I keep on getting `nil` on `verify`"
186
209
 
@@ -190,7 +213,7 @@ Poorly synchronized clocks will sometimes make the server think the token's `iat
190
213
 
191
214
  In case you need, here's a example of the payload structure from a Google login in JSON.
192
215
  ```json
193
- {
216
+ {
194
217
  "iss":"https://securetoken.google.com/{{YOUR_FIREBASE_APP_ID}}",
195
218
  "name":"Ugly Bob",
196
219
  "picture":"https://someurl.com/photo.jpg",
@@ -202,12 +225,12 @@ In case you need, here's a example of the payload structure from a Google login
202
225
  "exp":33029000017, // needs to be in the future
203
226
  "email":"uglybob@emailurl.com",
204
227
  "email_verified":true,
205
- "firebase":{
206
- "identities":{
207
- "google.com":[
228
+ "firebase":{
229
+ "identities":{
230
+ "google.com":[
208
231
  "1010101010101010101"
209
232
  ],
210
- "email":[
233
+ "email":[
211
234
  "uglybob@emailurl.com"
212
235
  ]
213
236
  },
@@ -232,6 +255,12 @@ Just run:
232
255
  FirebaseIdToken.test!
233
256
  ```
234
257
 
258
+ > **Warning**
259
+ > `FirebaseIdToken.test!` replaces signature verification with a fixture
260
+ > certificate whose private key is publicly available in this repository.
261
+ > Never call it outside of your test suite. An application running in test
262
+ > mode will accept forged tokens crafted by anyone.
263
+
235
264
  By using this mode, the following methods become available.
236
265
 
237
266
  ```ruby
@@ -268,7 +297,7 @@ module Api
268
297
  @routes = Engine.routes
269
298
  @user = users(:one)
270
299
  end
271
-
300
+
272
301
  def create_token(sub: nil)
273
302
  _payload = payload.merge({sub: sub})
274
303
  JWT.encode _payload, OpenSSL::PKey::RSA.new(FirebaseIdToken::Testing::Certificates.private_key), 'RS256'
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = 'A Firebase ID Token verifier.'
13
13
  spec.description = "A Ruby gem to verify the signature of Firebase ID "\
14
- "Tokens. It uses Redis to store Google's x509 certificates and manage "\
14
+ "Tokens. It uses ActiveSupport::Cache to store Google's x509 certificates and manage "\
15
15
  "their expiration time, so you don't need to request Google's API in "\
16
16
  "every execution and can access it as fast as reading from memory."
17
17
  spec.homepage = 'https://github.com/fschuindt/firebase_id_token'
@@ -29,10 +29,10 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'simplecov', '~> 0.22.0'
30
30
  spec.add_development_dependency 'simplecov_json_formatter', '~> 0.1.2'
31
31
  spec.add_development_dependency 'pry', '~> 0.14.2'
32
+ spec.add_development_dependency 'redis', '~> 5.0', '>= 5.0.6'
33
+ spec.add_development_dependency 'redis-namespace', '~> 1.10'
32
34
 
33
- spec.add_runtime_dependency 'redis', '~> 5.0', '>= 5.0.6'
34
- spec.add_runtime_dependency 'redis-namespace', '~> 1.10'
35
- spec.add_dependency 'httparty', '~> 0.21.0'
35
+ spec.add_dependency 'httparty', '>= 0.21', '< 1.0'
36
36
  spec.add_runtime_dependency 'jwt', '~> 2.7'
37
37
  spec.add_runtime_dependency 'activesupport', '~> 7.0', '>= 7.0.4.3'
38
38
  spec.add_runtime_dependency 'json', '~> 2.6', '>= 2.6.3'
@@ -0,0 +1,92 @@
1
+ module FirebaseIdToken
2
+ # ActiveSupport specific methods for Certificates class
3
+ class Certificates::ActiveSupport < Certificates
4
+ attr_reader :cache_store
5
+ RACE_CONDITION_TIME = 5.seconds
6
+
7
+ def initialize(source: :id_token)
8
+ @source = source
9
+ @cache_store = ::FirebaseIdToken.configuration.cache_store
10
+ @local_certs = read_certificates
11
+ end
12
+
13
+ def self.ttl(source: :id_token)
14
+ current_time = Time.now.to_i
15
+ entry = new(source: source).cache_store.read CACHE_KEYS.fetch(source)
16
+ if entry
17
+ expires_at = JSON.parse(entry)["expires_at"]
18
+ return expires_at - current_time < 0 ? 0 : expires_at - current_time
19
+ end
20
+ return 0
21
+ end
22
+
23
+ private
24
+
25
+ def read_certificates
26
+ entry = cache_store.read(cache_key)
27
+ if entry.nil?
28
+ lock do
29
+ request!
30
+ end
31
+ entry = cache_store.read(cache_key)
32
+ end
33
+ certs = {}
34
+ certs = JSON.parse(JSON.parse(entry)["data"]) if entry
35
+ certs
36
+ rescue StandardError => e
37
+ return {}
38
+ end
39
+
40
+ # we can't use ActiveSupport's fetch, because we need to set the expiration time of
41
+ # the key based on a value we read from the request. This is a rudimentary mutex instead
42
+ def lock
43
+ acquire_lock
44
+ yield
45
+ ensure
46
+ release_lock
47
+ end
48
+
49
+ def acquire_lock
50
+ maybe_sleep
51
+ cache_store.write(lock_key, true, expires_in: 5.seconds)
52
+ end
53
+
54
+ def maybe_sleep
55
+ iteration = 0
56
+ while cache_store.exist?(lock_key)
57
+ iteration += 1
58
+ sleep 1
59
+ break if iteration > 5
60
+ end
61
+ end
62
+
63
+ def release_lock
64
+ cache_store.delete(lock_key)
65
+ end
66
+
67
+ def lock_key
68
+ "#{cache_key}_lock"
69
+ end
70
+
71
+ def save_certificates
72
+ expires_at = Time.now.to_i + ttl
73
+ # set the expiration of the key to the certification expiration - RACE_CONDITION_TIME, so that the entry
74
+ # will be expired before the certificate is
75
+ cache_store.write cache_key, { data: @request.body, expires_at: expires_at }.to_json,
76
+ expires_in: (ttl - RACE_CONDITION_TIME)
77
+ @local_certs = @request.body
78
+ end
79
+
80
+ def ttl
81
+ cache_control = @request.headers['cache-control']
82
+ ttl = cache_control.match(/max-age=([0-9]+)/).captures.first.to_i
83
+
84
+ if ttl > 3600
85
+ ttl
86
+ else
87
+ raise ::FirebaseIdToken::Exceptions::CertificatesTtlError
88
+ end
89
+ end
90
+ end
91
+ end
92
+
@@ -0,0 +1,46 @@
1
+ module FirebaseIdToken
2
+ # Redis specific methods for Certificates class
3
+ class Certificates::Redis < Certificates
4
+ attr_reader :redis
5
+
6
+ def initialize(source: :id_token)
7
+ @source = source
8
+ @redis = ::Redis::Namespace.new('firebase_id_token',
9
+ redis: FirebaseIdToken.configuration.redis)
10
+ @local_certs = read_certificates
11
+ end
12
+
13
+ # Returns the current certificates TTL (Time-To-Live) in seconds. *Zero
14
+ # meaning no certificates.* It's the same as the certificates expiration
15
+ # time, use it to know when to request again.
16
+ # @return [Fixnum]
17
+ def self.ttl(source: :id_token)
18
+ ttl = new(source: source).redis.ttl(CACHE_KEYS.fetch(source))
19
+ ttl < 0 ? 0 : ttl
20
+ end
21
+
22
+ private
23
+
24
+ def read_certificates
25
+ certs = @redis.get cache_key
26
+ certs ? JSON.parse(certs) : {}
27
+ end
28
+
29
+ def save_certificates
30
+ @redis.setex cache_key, ttl, @request.body
31
+ @local_certs = read_certificates
32
+ end
33
+
34
+ def ttl
35
+ cache_control = @request.headers['cache-control']
36
+ ttl = cache_control.match(/max-age=([0-9]+)/).captures.first.to_i
37
+
38
+ if ttl > 3600
39
+ ttl
40
+ else
41
+ raise Exceptions::CertificatesTtlError
42
+ end
43
+ end
44
+ end
45
+ end
46
+