firebase_id_token 2.5.2 → 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 +4 -4
- data/.github/workflows/test.yml +28 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +71 -1
- data/Gemfile +7 -0
- data/README.md +97 -56
- data/firebase_id_token.gemspec +14 -12
- data/lib/firebase_id_token/certificates/active_support.rb +92 -0
- data/lib/firebase_id_token/certificates/redis.rb +46 -0
- data/lib/firebase_id_token/certificates.rb +69 -60
- data/lib/firebase_id_token/configuration.rb +10 -3
- data/lib/firebase_id_token/exceptions/unsupported_cache_operation_error.rb +11 -0
- data/lib/firebase_id_token/signature.rb +30 -13
- data/lib/firebase_id_token/testing/certificates.rb +1 -1
- data/lib/firebase_id_token/version.rb +1 -1
- data/lib/firebase_id_token.rb +8 -6
- data/spec/firebase_id_token/certificates/active_support_spec.rb +43 -0
- data/spec/firebase_id_token/certificates/redis_spec.rb +39 -0
- data/spec/firebase_id_token/certificates_session_cookie_spec.rb +49 -0
- data/spec/firebase_id_token/configuration_spec.rb +0 -6
- data/spec/firebase_id_token/signature_session_cookie_spec.rb +65 -0
- data/spec/firebase_id_token/signature_spec.rb +15 -1
- data/spec/firebase_id_token/signature_test_spec.rb +4 -0
- data/spec/firebase_id_token_spec.rb +1 -1
- data/spec/spec_helper.rb +4 -0
- data/spec/support/certificates_shared_examples.rb +196 -0
- metadata +81 -60
- data/CODE_OF_CONDUCT.md +0 -74
- data/spec/firebase_id_token/certificates_spec.rb +0 -164
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7737e97a521a870ff07e83c63f2dbf0d72e89ba85858f42ae9443762518a15a
|
|
4
|
+
data.tar.gz: 20f27c0f72411d19d6f0efb29fb48528a31c5b6a9ccee7d42300ff61f08833c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e120f07c5e5017932ab02dcd7955bc3ee292ed360f6da45f56f739394488aa8ae41bab42ab30dbd1f4dd7f51e3dfeed6c3d3a1acd00925656e87be8253a3cda
|
|
7
|
+
data.tar.gz: a84b333fc8a2a9ad2cec07300ba4ad46a72b9685cebb9cfd0dcb7e2651a123cfed5732487851b76e0a0f8a2ef897989e417418eecf0d38375b287e582a0dd0c9
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: "Run Tests"
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
services:
|
|
13
|
+
redis:
|
|
14
|
+
image: redis
|
|
15
|
+
ports:
|
|
16
|
+
- 6379:6379
|
|
17
|
+
options: --entrypoint redis-server
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v3
|
|
20
|
+
- uses: ruby/setup-ruby@v1
|
|
21
|
+
with:
|
|
22
|
+
ruby-version: '3.2.2'
|
|
23
|
+
- name: Install Bundler
|
|
24
|
+
run: gem install bundler:2.4.13
|
|
25
|
+
- name: Install Dependencies
|
|
26
|
+
run: bundle install
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: bundle exec rspec
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.2.2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
## [
|
|
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.
|
|
43
|
+
|
|
44
|
+
## [3.0.0] - 2023-04-11
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- CI testing using GitHub Actions.
|
|
48
|
+
- Explains on `README.md` about the behavior of `verify` on expired tokens. [Details here](https://github.com/fschuindt/firebase_id_token/issues/29).
|
|
49
|
+
- Warns about the poorly synchronized clocks issue with the token's `iat`. [Details here](https://github.com/fschuindt/firebase_id_token/issues/21#issuecomment-623133926).
|
|
50
|
+
- Gives better examples when testing. [Details here](https://github.com/fschuindt/firebase_id_token/pull/38).
|
|
51
|
+
- Created a `.ruby-version` file.
|
|
52
|
+
- Added ActiveSupport as dependency for `Time.current`.
|
|
53
|
+
- SimpleCov JSON formatter and `json` as dependency.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- Redis is no longer required. Any ActiveSupport cache will now work.
|
|
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).
|
|
58
|
+
- Upgraded Redis to 5.0.6.
|
|
59
|
+
- Upgraded Redis Namespace to 1.10.
|
|
60
|
+
- Upgraded HTTParty to 0.21.0.
|
|
61
|
+
- Upgraded JWT to 2.7.
|
|
62
|
+
- Upgraded [Dev] Ruby to 3.2.2.
|
|
63
|
+
- Upgraded [Dev] Bundler to 2.4.13.
|
|
64
|
+
- Upgraded [Dev] Rake to 13.0.6.
|
|
65
|
+
- Upgraded [Dev] RSpec to 3.12.
|
|
66
|
+
- Upgraded [Dev] Redcarpet to 3.6.
|
|
67
|
+
- Upgraded [Dev] Simplecov to 0.22.0.
|
|
68
|
+
- Upgraded [Dev] Pry to 0.14.2.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
- Code Climate test coverage report.
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
- Travis CI badge.
|
|
75
|
+
|
|
76
|
+
## [2.5.2] - 2023-04-09
|
|
10
77
|
|
|
11
78
|
### Fixed
|
|
12
79
|
- [CWE-472](https://github.com/fschuindt/firebase_id_token/pull/41).
|
|
@@ -119,6 +186,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
119
186
|
## [0.1.0] - 2017-04-23
|
|
120
187
|
*Version removed.*
|
|
121
188
|
|
|
189
|
+
[4.0.0]: https://github.com/fschuindt/firebase_id_token/compare/3.0.0...4.0.0
|
|
190
|
+
[3.0.0]: https://github.com/fschuindt/firebase_id_token/compare/2.5.2...3.0.0
|
|
191
|
+
[2.5.2]: https://github.com/fschuindt/firebase_id_token/compare/2.5.1...2.5.2
|
|
122
192
|
[2.5.1]: https://github.com/fschuindt/firebase_id_token/compare/2.5.0...2.5.1
|
|
123
193
|
[2.5.0]: https://github.com/fschuindt/firebase_id_token/compare/2.4.0...2.5.0
|
|
124
194
|
[2.4.0]: https://github.com/fschuindt/firebase_id_token/compare/2.3.2...2.4.0
|
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
|
|
1
|
+
# Ruby Firebase ID Token verifier
|
|
2
2
|
|
|
3
|
-
](https://codeclimate.com/github/fschuindt/firebase_id_token)
|
|
5
|
-
[](https://codeclimate.com/github/fschuindt/firebase_id_token)
|
|
6
|
-
[](https://codeclimate.com/github/fschuindt/firebase_id_token/coverage)
|
|
7
|
-
[](http://inch-ci.org/github/fschuindt/firebase_id_token)
|
|
3
|
+
[](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. It uses
|
|
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', '~>
|
|
24
|
+
gem 'firebase_id_token', '~> 4.0.0'
|
|
33
25
|
```
|
|
34
26
|
then
|
|
35
27
|
```
|
|
@@ -43,24 +35,29 @@ It's needed to set up your Firebase Project ID.
|
|
|
43
35
|
If you are using Rails, this should probably go into `config/initializers/firebase_id_token.rb`.
|
|
44
36
|
```ruby
|
|
45
37
|
FirebaseIdToken.configure do |config|
|
|
38
|
+
config.cache_store = ActiveSupport::Cache::RedisCacheStore.new
|
|
46
39
|
config.project_ids = ['your-firebase-project-id']
|
|
47
40
|
end
|
|
48
41
|
```
|
|
49
42
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*If you want to verify signatures from more than one Firebase project, just add more Project IDs to the list.*
|
|
53
|
-
|
|
54
|
-
You can also pass a Redis instance to `config` if you are not using Redis defaults.
|
|
55
|
-
In this case, you must have the gem `redis` in your `Gemfile`.
|
|
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)
|
|
56
45
|
```ruby
|
|
57
46
|
FirebaseIdToken.configure do |config|
|
|
47
|
+
config.redis = Redis.new
|
|
58
48
|
config.project_ids = ['your-firebase-project-id']
|
|
59
|
-
config.redis = Redis.new(host: '10.0.1.1', port: 6380, db: 15)
|
|
60
49
|
end
|
|
61
50
|
```
|
|
62
51
|
|
|
63
|
-
|
|
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`
|
|
58
|
+
- `project_ids` must be an Array.
|
|
59
|
+
|
|
60
|
+
*If you want to verify signatures from more than one Firebase project, just add more Project IDs to the list.*
|
|
64
61
|
|
|
65
62
|
## Usage
|
|
66
63
|
|
|
@@ -71,45 +68,47 @@ really helpful. But here is a complete guide:
|
|
|
71
68
|
|
|
72
69
|
Before verifying tokens, you need to download Google's x509 certificates.
|
|
73
70
|
|
|
74
|
-
To do it simply:
|
|
71
|
+
To do it, simply:
|
|
75
72
|
```ruby
|
|
76
73
|
FirebaseIdToken::Certificates.request
|
|
77
74
|
```
|
|
78
75
|
|
|
79
|
-
It will download the certificates and save it in
|
|
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:
|
|
80
77
|
```ruby
|
|
81
78
|
FirebaseIdToken::Certificates.request!
|
|
82
79
|
```
|
|
83
80
|
|
|
84
|
-
Google give us information about the certificates expiration time, it's used to set a
|
|
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.
|
|
85
82
|
|
|
86
83
|
#### Certificates Info
|
|
87
84
|
|
|
88
|
-
Checks the presence of certificates in
|
|
85
|
+
Checks the presence of certificates in the cache.
|
|
89
86
|
```ruby
|
|
90
87
|
FirebaseIdToken::Certificates.present?
|
|
91
88
|
=> true
|
|
92
89
|
```
|
|
93
90
|
|
|
94
|
-
How many seconds until the certificate's expiration.
|
|
91
|
+
How many seconds until the certificate's expiration. _NOTE_: Currently only functional when using Redis
|
|
95
92
|
```ruby
|
|
96
93
|
FirebaseIdToken::Certificates.ttl
|
|
97
94
|
=> 22352
|
|
98
95
|
```
|
|
99
96
|
|
|
100
|
-
Lists all certificates in
|
|
97
|
+
Lists all certificates in the database.
|
|
101
98
|
```ruby
|
|
102
99
|
FirebaseIdToken::Certificates.all
|
|
103
100
|
=> [{"ec8f292sd30224afac5c55540df66d1f999d" => <OpenSSL::X509::Certificate: [...]]
|
|
104
101
|
```
|
|
105
102
|
|
|
106
|
-
Finds the respective certificate of a given Key ID.
|
|
103
|
+
Finds the respective certificate of a given Key ID (`kid`).
|
|
107
104
|
```ruby
|
|
108
105
|
FirebaseIdToken::Certificates.find('ec8f292sd30224afac5c55540df66d1f999d')
|
|
109
106
|
=> <OpenSSL::X509::Certificate: subject=<OpenSSL::X509 [...]>
|
|
110
107
|
```
|
|
111
108
|
|
|
112
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.
|
|
113
112
|
|
|
114
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).
|
|
115
114
|
|
|
@@ -121,12 +120,12 @@ Create your task in `lib/tasks/firebase.rake`:
|
|
|
121
120
|
```ruby
|
|
122
121
|
namespace :firebase do
|
|
123
122
|
namespace :certificates do
|
|
124
|
-
desc "Request Google's x509 certificates when
|
|
123
|
+
desc "Request Google's x509 certificates when the cache is empty"
|
|
125
124
|
task request: :environment do
|
|
126
125
|
FirebaseIdToken::Certificates.request
|
|
127
126
|
end
|
|
128
127
|
|
|
129
|
-
desc "Request Google's x509 certificates and override
|
|
128
|
+
desc "Request Google's x509 certificates and override the cache"
|
|
130
129
|
task force_request: :environment do
|
|
131
130
|
FirebaseIdToken::Certificates.request!
|
|
132
131
|
end
|
|
@@ -153,7 +152,7 @@ When developing, you should just run the task:
|
|
|
153
152
|
$ rake firebase:certificates:request
|
|
154
153
|
```
|
|
155
154
|
|
|
156
|
-
*
|
|
155
|
+
*You need Redis to be running if you're using RedisCacheStore*
|
|
157
156
|
|
|
158
157
|
### Verifying Tokens
|
|
159
158
|
|
|
@@ -173,30 +172,65 @@ FirebaseIdToken::Signature.verify('aaaaaa')
|
|
|
173
172
|
=> nil
|
|
174
173
|
```
|
|
175
174
|
|
|
176
|
-
|
|
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
|
+
|
|
190
|
+
#### WARNING!
|
|
191
|
+
|
|
192
|
+
##### Expired tokens can point to long gone certificates
|
|
193
|
+
|
|
194
|
+
Notice that often when the token have expired, the Firebase certificate can be already missing from the Firebase servers. In these cases, `verify` will return `nil`.
|
|
195
|
+
|
|
196
|
+
If you want to take specific actions in such cases, here's a solution suggested by the user [cfanpnk](https://github.com/fschuindt/firebase_id_token/issues/29#issuecomment-751137511):
|
|
197
|
+
|
|
198
|
+
1. Use `verify!` to raise an exception.
|
|
199
|
+
2. Rescue `FirebaseIdToken::Exceptions::CertificateNotFound` and return `401`.
|
|
200
|
+
3. The client app will refresh the token if expired.
|
|
201
|
+
|
|
202
|
+
More details [here](https://github.com/fschuindt/firebase_id_token/issues/29).
|
|
203
|
+
|
|
204
|
+
##### Trying to verify tokens without downloaded certificates will raise an error
|
|
205
|
+
|
|
206
|
+
If you try to verify a signature without any certificates in the cache, it will raise a `FirebaseIdToken::Exceptions::NoCertificatesError`.
|
|
207
|
+
|
|
208
|
+
##### "I keep on getting `nil` on `verify`"
|
|
209
|
+
|
|
210
|
+
Poorly synchronized clocks will sometimes make the server think the token's `iat` is on the future, which will render the token as invalid. Make sure your server's or development system's clock is correctly set. On macOS, some people reported success by unchecking and checking the "Set date and time automatically" configuration checkbox. See [here](https://github.com/fschuindt/firebase_id_token/issues/21#issuecomment-623133926).
|
|
177
211
|
|
|
178
212
|
#### Payload Structure
|
|
179
213
|
|
|
180
214
|
In case you need, here's a example of the payload structure from a Google login in JSON.
|
|
181
215
|
```json
|
|
182
|
-
{
|
|
183
|
-
"iss":"https://securetoken.google.com/
|
|
216
|
+
{
|
|
217
|
+
"iss":"https://securetoken.google.com/{{YOUR_FIREBASE_APP_ID}}",
|
|
184
218
|
"name":"Ugly Bob",
|
|
185
219
|
"picture":"https://someurl.com/photo.jpg",
|
|
186
|
-
"aud":"
|
|
220
|
+
"aud":"{{YOUR_FIREBASE_APP_ID}}",
|
|
187
221
|
"auth_time":1492981192,
|
|
188
222
|
"user_id":"theUserID",
|
|
189
223
|
"sub":"theUserID",
|
|
190
|
-
"iat":1492981200,
|
|
191
|
-
"exp":33029000017,
|
|
224
|
+
"iat":1492981200, // needs to be in the past
|
|
225
|
+
"exp":33029000017, // needs to be in the future
|
|
192
226
|
"email":"uglybob@emailurl.com",
|
|
193
227
|
"email_verified":true,
|
|
194
|
-
"firebase":{
|
|
195
|
-
"identities":{
|
|
196
|
-
"google.com":[
|
|
228
|
+
"firebase":{
|
|
229
|
+
"identities":{
|
|
230
|
+
"google.com":[
|
|
197
231
|
"1010101010101010101"
|
|
198
232
|
],
|
|
199
|
-
"email":[
|
|
233
|
+
"email":[
|
|
200
234
|
"uglybob@emailurl.com"
|
|
201
235
|
]
|
|
202
236
|
},
|
|
@@ -206,34 +240,42 @@ In case you need, here's a example of the payload structure from a Google login
|
|
|
206
240
|
|
|
207
241
|
```
|
|
208
242
|
|
|
243
|
+
If you're using this snippet for testing, make sure you check the comments in it.
|
|
209
244
|
|
|
210
|
-
##
|
|
211
|
-
The test suite can be run with `bundle exec rake rspec`
|
|
245
|
+
## Testing
|
|
212
246
|
|
|
247
|
+
```
|
|
248
|
+
bundle exec rake rspec
|
|
249
|
+
```
|
|
213
250
|
|
|
214
|
-
|
|
251
|
+
### Testing Mode
|
|
215
252
|
|
|
216
|
-
|
|
253
|
+
Just run:
|
|
254
|
+
```
|
|
255
|
+
FirebaseIdToken.test!
|
|
256
|
+
```
|
|
217
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.
|
|
218
263
|
|
|
219
|
-
By using
|
|
264
|
+
By using this mode, the following methods become available.
|
|
220
265
|
|
|
221
266
|
```ruby
|
|
222
267
|
# RSA PRIVATE KEY
|
|
223
268
|
FirebaseIdToken::Testing::Certificates.private_key
|
|
269
|
+
|
|
224
270
|
# CERTIFICATE
|
|
225
271
|
FirebaseIdToken::Testing::Certificates.certificate
|
|
226
272
|
```
|
|
227
273
|
|
|
228
|
-
|
|
274
|
+
`certificate` will always return the same value. No external HTTP call is performed.
|
|
229
275
|
|
|
276
|
+
#### Example: Testing in Rails
|
|
230
277
|
|
|
231
|
-
|
|
232
|
-
#### Rails test
|
|
233
|
-
|
|
234
|
-
Describe the following in test_helper.rb etc.
|
|
235
|
-
|
|
236
|
-
* test_helper
|
|
278
|
+
Describes the following in `test_helper.rb`.
|
|
237
279
|
|
|
238
280
|
```ruby
|
|
239
281
|
class ActiveSupport::TestCase
|
|
@@ -243,7 +285,7 @@ class ActiveSupport::TestCase
|
|
|
243
285
|
end
|
|
244
286
|
```
|
|
245
287
|
|
|
246
|
-
|
|
288
|
+
Test example:
|
|
247
289
|
|
|
248
290
|
```ruby
|
|
249
291
|
require 'test_helper'
|
|
@@ -255,7 +297,7 @@ module Api
|
|
|
255
297
|
@routes = Engine.routes
|
|
256
298
|
@user = users(:one)
|
|
257
299
|
end
|
|
258
|
-
|
|
300
|
+
|
|
259
301
|
def create_token(sub: nil)
|
|
260
302
|
_payload = payload.merge({sub: sub})
|
|
261
303
|
JWT.encode _payload, OpenSSL::PKey::RSA.new(FirebaseIdToken::Testing::Certificates.private_key), 'RS256'
|
|
@@ -274,7 +316,6 @@ module Api
|
|
|
274
316
|
end
|
|
275
317
|
```
|
|
276
318
|
|
|
277
|
-
|
|
278
319
|
## License
|
|
279
320
|
|
|
280
321
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/firebase_id_token.gemspec
CHANGED
|
@@ -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
|
|
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'
|
|
@@ -22,16 +22,18 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
23
23
|
spec.require_paths = ['lib']
|
|
24
24
|
|
|
25
|
-
spec.add_development_dependency 'bundler', '~> 2.
|
|
26
|
-
spec.add_development_dependency 'rake', '~>
|
|
27
|
-
spec.add_development_dependency 'rspec', '~> 3.
|
|
28
|
-
spec.add_development_dependency 'redcarpet', '~> 3.
|
|
29
|
-
spec.add_development_dependency 'simplecov', '~> 0.
|
|
30
|
-
spec.add_development_dependency '
|
|
31
|
-
spec.add_development_dependency 'pry', '~> 0.
|
|
25
|
+
spec.add_development_dependency 'bundler', '~> 2.4', '>= 2.4.13'
|
|
26
|
+
spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.6'
|
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
28
|
+
spec.add_development_dependency 'redcarpet', '~> 3.6'
|
|
29
|
+
spec.add_development_dependency 'simplecov', '~> 0.22.0'
|
|
30
|
+
spec.add_development_dependency 'simplecov_json_formatter', '~> 0.1.2'
|
|
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.
|
|
34
|
-
spec.add_runtime_dependency '
|
|
35
|
-
spec.
|
|
36
|
-
spec.add_runtime_dependency '
|
|
35
|
+
spec.add_dependency 'httparty', '>= 0.21', '< 1.0'
|
|
36
|
+
spec.add_runtime_dependency 'jwt', '~> 2.7'
|
|
37
|
+
spec.add_runtime_dependency 'activesupport', '~> 7.0', '>= 7.0.4.3'
|
|
38
|
+
spec.add_runtime_dependency 'json', '~> 2.6', '>= 2.6.3'
|
|
37
39
|
end
|
|
@@ -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
|
+
|