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.
@@ -1,22 +1,22 @@
1
1
  module FirebaseIdToken
2
2
  # Manage download and access of Google's x509 certificates. Keeps
3
- # certificates on a Redis namespace database.
3
+ # certificates on an ActiveSupport cache.
4
4
  #
5
5
  # ## Download & Access Certificates
6
6
  #
7
7
  # It describes two ways to download it: {.request} and {.request!}.
8
- # The first will only do something when Redis certificates database is empty,
8
+ # The first will only do something when the certificates cache is empty,
9
9
  # the second one will always request a new download to Google's API and
10
10
  # override the database with the response.
11
11
  #
12
12
  # It's important to note that when saving a set of certificates, it will also
13
- # set a Redis expiration time to match Google's API header `expires`. **After
14
- # this time went out, Redis will automatically delete those certificates.**
13
+ # set a expiration time to match Google's API header `expires`. **After
14
+ # this time went out, the cache will no longer provide those certificates.**
15
15
  #
16
16
  # *To know how many seconds left until the expiration you can use {.ttl}.*
17
17
  #
18
18
  # When comes to accessing it, you can either use {.present?} to check if
19
- # there's any data inside Redis certificates database or {.all} to obtain an
19
+ # there's any data inside the cache or {.all} to obtain an
20
20
  # `Array` of current certificates.
21
21
  #
22
22
  # @example `.request` will only download once
@@ -30,30 +30,46 @@ module FirebaseIdToken
30
30
  # FirebaseIdToken::Certificates.request! # Downloads certificates.
31
31
  #
32
32
  class Certificates
33
- # A Redis instance.
34
- attr_reader :redis
35
- # Certificates saved in the Redis (JSON `String` or `nil`).
33
+ # Certificates saved in the cache (JSON `String` or `nil`).
36
34
  attr_reader :local_certs
37
35
 
38
- # Google's x509 certificates API URL.
36
+ # Certificate source (`:id_token` or `:session_cookie`).
37
+ attr_reader :source
38
+
39
+ # Google's x509 certificates API URL for ID Tokens.
39
40
  URL = 'https://www.googleapis.com/robot/v1/metadata/x509/'\
40
41
  'securetoken@system.gserviceaccount.com'
41
42
 
42
- # Calls {.request!} only if there are no certificates on Redis. It will
43
+ # Google's x509 certificates API URL for Session Cookies. Session Cookies
44
+ # are signed by a different key set than ID Tokens.
45
+ SESSION_COOKIE_URL = 'https://www.googleapis.com/identitytoolkit/v3/'\
46
+ 'relyingparty/publicKeys'
47
+
48
+ # Certificates API URL of each source.
49
+ URLS = { id_token: URL, session_cookie: SESSION_COOKIE_URL }.freeze
50
+
51
+ # Cache key of each source, so both certificate sets live side by side.
52
+ CACHE_KEYS = {
53
+ id_token: 'certificates',
54
+ session_cookie: 'session_cookie_certificates'
55
+ }.freeze
56
+
57
+ # Calls {.request!} only if there are no certificates in the cache. It will
43
58
  # return `nil` otherwise.
44
59
  #
45
60
  # It will raise {Exceptions::CertificatesRequestError} if the request
46
61
  # fails or {Exceptions::CertificatesTtlError} when Google responds with a
47
62
  # low TTL, check out {.request!} for more info.
48
63
  #
64
+ # @param [Symbol] source `:id_token` (default) or `:session_cookie`
49
65
  # @return [nil, Hash]
50
66
  # @see Certificates.request!
51
- def self.request
52
- new.request
67
+ def self.request(source: :id_token)
68
+ new_child(source: source).request
53
69
  end
54
70
 
55
71
  # Triggers a HTTPS request to Google's x509 certificates API. If it
56
- # responds with a status `200 OK`, saves the request body into Redis and
72
+ # responds with a status `200 OK`, saves the request body into the cache and
57
73
  # returns it as a `Hash`.
58
74
  #
59
75
  # Otherwise it will raise a {Exceptions::CertificatesRequestError}.
@@ -61,9 +77,10 @@ module FirebaseIdToken
61
77
  # This is really rare to happen, but Google may respond with a low TTL
62
78
  # certificate. This is a `SecurityError` and will raise a
63
79
  # {Exceptions::CertificatesTtlError}. You are mostly like to never face it.
80
+ # @param [Symbol] source `:id_token` (default) or `:session_cookie`
64
81
  # @return [Hash]
65
- def self.request!
66
- new.request!
82
+ def self.request!(source: :id_token)
83
+ new_child(source: source).request!
67
84
  end
68
85
 
69
86
  # @deprecated Use only `request!` in favor of Ruby conventions.
@@ -73,46 +90,46 @@ module FirebaseIdToken
73
90
  warn 'WARNING: FirebaseIdToken::Certificates.request_anyway is '\
74
91
  'deprecated. Use FirebaseIdToken::Certificates.request! instead.'
75
92
 
76
- new.request!
93
+ new_child.request!
77
94
  end
78
95
 
79
- # Returns `true` if there's certificates data on Redis, `false` otherwise.
96
+ # Returns `true` if there's certificates data in the cache, `false` otherwise.
80
97
  # @example
81
98
  # FirebaseIdToken::Certificates.present? #=> false
82
99
  # FirebaseIdToken::Certificates.request
83
100
  # FirebaseIdToken::Certificates.present? #=> true
84
- def self.present?
85
- ! new.local_certs.empty?
101
+ def self.present?(source: :id_token)
102
+ ! new_child(source: source).local_certs.empty?
86
103
  end
87
104
 
88
105
  # Returns an array of hashes, each hash is a single `{key => value}` pair
89
106
  # containing the certificate KID `String` as key and a
90
107
  # `OpenSSL::X509::Certificate` object of the respective certificate as
91
- # value. Returns a empty `Array` when there's no certificates data on
92
- # Redis.
108
+ # value. Returns a empty `Array` when there's no certificates data in
109
+ # the cache.
93
110
  # @return [Array]
94
111
  # @example
95
112
  # FirebaseIdToken::Certificates.request
96
113
  # certs = FirebaseIdToken::Certificates.all
97
114
  # certs.first #=> {"1d6d01c7[...]" => #<OpenSSL::X509::Certificate[...]}
98
- def self.all
99
- new.local_certs.map { |kid, cert|
115
+ def self.all(source: :id_token)
116
+ new_child(source: source).local_certs.map { |kid, cert|
100
117
  { kid => OpenSSL::X509::Certificate.new(cert) } }
101
118
  end
102
119
 
103
120
  # Returns a `OpenSSL::X509::Certificate` object of the requested Key ID
104
121
  # (KID) if there's one. Returns `nil` otherwise.
105
122
  #
106
- # It will raise a {Exceptions::NoCertificatesError} if the Redis
107
- # certificates database is empty.
123
+ # It will raise a {Exceptions::NoCertificatesError} if the
124
+ # certificates cache is empty.
108
125
  # @param [String] kid Key ID
109
126
  # @return [nil, OpenSSL::X509::Certificate]
110
127
  # @example
111
128
  # FirebaseIdToken::Certificates.request
112
129
  # cert = FirebaseIdToken::Certificates.find "1d6d01f4w7d54c7[...]"
113
130
  # #=> <OpenSSL::X509::Certificate: subject=#<OpenSSL [...]
114
- def self.find(kid, raise_error: false)
115
- certs = new.local_certs
131
+ def self.find(kid, raise_error: false, source: :id_token)
132
+ certs = new_child(source: source).local_certs
116
133
  raise Exceptions::NoCertificatesError if certs.empty?
117
134
 
118
135
  return OpenSSL::X509::Certificate.new certs[kid] if certs[kid]
@@ -128,8 +145,8 @@ module FirebaseIdToken
128
145
  #
129
146
  # @raise {Exceptions::CertificateNotFound} if it cannot be found.
130
147
  #
131
- # @raise {Exceptions::NoCertificatesError} if the Redis certificates
132
- # database is empty.
148
+ # @raise {Exceptions::NoCertificatesError} if the certificates cache
149
+ # is empty.
133
150
  #
134
151
  # @param [String] kid Key ID
135
152
  # @return [OpenSSL::X509::Certificate]
@@ -137,26 +154,33 @@ module FirebaseIdToken
137
154
  # FirebaseIdToken::Certificates.request
138
155
  # cert = FirebaseIdToken::Certificates.find! "1d6d01f4w7d54c7[...]"
139
156
  # #=> <OpenSSL::X509::Certificate: subject=#<OpenSSL [...]
140
- def self.find!(kid)
141
- find(kid, raise_error: true)
157
+ def self.find!(kid, source: :id_token)
158
+ find(kid, raise_error: true, source: source)
142
159
  end
143
160
 
144
161
  # Returns the current certificates TTL (Time-To-Live) in seconds. *Zero
145
162
  # meaning no certificates.* It's the same as the certificates expiration
146
163
  # time, use it to know when to request again.
147
164
  # @return [Fixnum]
148
- def self.ttl
149
- ttl = new.redis.ttl('certificates')
150
- ttl < 0 ? 0 : ttl
165
+ def self.ttl(source: :id_token)
166
+ # call a child class based on the configuration
167
+ FirebaseIdToken.configuration.klass.ttl(source: source)
168
+ end
169
+
170
+ # When called on Certificates itself, picks the store class from the
171
+ # configuration. When called on a subclass, instantiates that subclass.
172
+ def self.new_child(source: :id_token)
173
+ return new(source: source) unless self == Certificates
174
+
175
+ FirebaseIdToken.configuration.klass.new(source: source)
151
176
  end
152
177
 
153
- # Sets two instance attributes: `:redis` and `:local_certs`. Those are
154
- # respectively a Redis instance from {FirebaseIdToken::Configuration} and
178
+ # Sets two instance attributes: `:cach_store` and `:local_certs`. Those are
179
+ # respectively a cache instance from {FirebaseIdToken::Configuration} and
155
180
  # the certificates in it.
156
- def initialize
157
- @redis = Redis::Namespace.new('firebase_id_token',
158
- redis: FirebaseIdToken.configuration.redis)
159
- @local_certs = read_certificates
181
+ def initialize(source: :id_token)
182
+ # this should not be called directly. Call a child class
183
+ raise NotImplementedError
160
184
  end
161
185
 
162
186
  # @see Certificates.request
@@ -166,7 +190,7 @@ module FirebaseIdToken
166
190
 
167
191
  # @see Certificates.request!
168
192
  def request!
169
- @request = HTTParty.get URL
193
+ @request = HTTParty.get URLS.fetch(@source)
170
194
  code = @request.code
171
195
  if code == 200
172
196
  save_certificates
@@ -177,25 +201,10 @@ module FirebaseIdToken
177
201
 
178
202
  private
179
203
 
180
- def read_certificates
181
- certs = @redis.get 'certificates'
182
- certs ? JSON.parse(certs) : {}
183
- end
184
-
185
- def save_certificates
186
- @redis.setex 'certificates', ttl, @request.body
187
- @local_certs = read_certificates
188
- end
189
-
190
- def ttl
191
- cache_control = @request.headers['cache-control']
192
- ttl = cache_control.match(/max-age=([0-9]+)/).captures.first.to_i
193
-
194
- if ttl > 3600
195
- ttl
196
- else
197
- raise Exceptions::CertificatesTtlError
198
- end
204
+ # Cache key of the certificate source, so ID Token and Session Cookie
205
+ # certificates never mix.
206
+ def cache_key
207
+ CACHE_KEYS.fetch(@source)
199
208
  end
200
209
  end
201
210
  end
@@ -4,12 +4,19 @@ module FirebaseIdToken
4
4
  LIB_PATH = File.expand_path('../../', __FILE__)
5
5
 
6
6
  class Configuration
7
- attr_accessor :redis, :project_ids, :certificates
7
+ attr_accessor :redis, :project_ids, :cache_store
8
+ attr_writer :certificates
8
9
 
9
10
  def initialize
10
- @redis = Redis.new
11
11
  @project_ids = []
12
- @certificates = FirebaseIdToken::Certificates
12
+ end
13
+
14
+ def certificates
15
+ @certificates || klass
16
+ end
17
+
18
+ def klass
19
+ redis ? FirebaseIdToken::Certificates::Redis : FirebaseIdToken::Certificates::ActiveSupport
13
20
  end
14
21
  end
15
22
  end
@@ -0,0 +1,11 @@
1
+ module FirebaseIdToken
2
+ module Exceptions
3
+ # @see FirebaseIdToken::Certificates.request
4
+ # @see FirebaseIdToken::Certificates.request!
5
+ class UnsupportedCacheOperationError < StandardError
6
+ def initialize(message = "Cache store does not support a TTL read on entries")
7
+ super message
8
+ end
9
+ end
10
+ end
11
+ end
@@ -10,13 +10,13 @@ module FirebaseIdToken
10
10
  #
11
11
  # ## Verifying a Firebase ID Token
12
12
  #
13
- # *Be sure to configure the gem to set your Firebase Project ID and a Redis
14
- # server before move any forward.*
13
+ # *Be sure to configure the gem to set your Firebase Project ID cache
14
+ # store.*
15
15
  #
16
16
  # **See the README for a complete guide.**
17
17
  #
18
18
  # **WARNING:** Trying to verify a token without any certificate saved in
19
- # Redis certificates database raises a {Exceptions::NoCertificatesError}.
19
+ # the cache raises a {Exceptions::NoCertificatesError}.
20
20
  #
21
21
  # @example
22
22
  # FirebaseIdToken::Signature.verify(thrusty_token)
@@ -31,6 +31,12 @@ module FirebaseIdToken
31
31
  # [here](https://goo.gl/uOK5Jx).
32
32
  JWT_DEFAULTS = { algorithm: 'RS256', verify_iat: true }
33
33
 
34
+ # Issuer base URL of each token type.
35
+ ISSUER_BASE_URLS = {
36
+ id_token: 'https://securetoken.google.com',
37
+ session_cookie: 'https://session.firebase.google.com'
38
+ }.freeze
39
+
34
40
  # Returns the decoded JWT hash payload of the Firebase ID Token if the
35
41
  # signature in the token matches with one of the certificates downloaded
36
42
  # by {FirebaseIdToken::Certificates.request}, returns `nil` otherwise.
@@ -39,8 +45,8 @@ module FirebaseIdToken
39
45
  # JWT fields are valid, as recommended [here](https://goo.gl/yOrZZX) by
40
46
  # Firebase official documentation.
41
47
  #
42
- # Note that it will raise a {Exceptions::NoCertificatesError} if the Redis
43
- # certificates database is empty. Ensure to call {Certificates.request}
48
+ # Note that it will raise a {Exceptions::NoCertificatesError} if the cache
49
+ # is empty. Ensure to call {Certificates.request}
44
50
  # before, ideally in a background job if you are using Rails.
45
51
  #
46
52
  # If you would like this to raise and error, rather than silently failing,
@@ -49,18 +55,26 @@ module FirebaseIdToken
49
55
  # FirebaseIdToken::Signature
50
56
  # .verify(token, raise_error: Rails.env.development?)
51
57
  #
58
+ # Use `type: :session_cookie` to verify a Firebase Session Cookie instead
59
+ # of an ID Token. Session Cookies are issued by
60
+ # `https://session.firebase.google.com` and signed by a different
61
+ # certificate set. Example:
62
+ #
63
+ # FirebaseIdToken::Signature.verify(cookie, type: :session_cookie)
64
+ #
52
65
  # @param raise_error [Boolean] default: false
66
+ # @param type [Symbol] `:id_token` (default) or `:session_cookie`
53
67
  # @return [nil, Hash]
54
- def self.verify(jwt_token, raise_error: false)
55
- new(jwt_token, raise_error: raise_error).verify
68
+ def self.verify(jwt_token, raise_error: false, type: :id_token)
69
+ new(jwt_token, raise_error: raise_error, type: type).verify
56
70
  end
57
71
 
58
72
  # Equivalent to `.verify(jwt_token, raise_error: true)`.
59
73
  #
60
74
  # @see {Signature.verify}
61
75
  # @return [Hash]
62
- def self.verify!(jwt_token)
63
- new(jwt_token, raise_error: true).verify
76
+ def self.verify!(jwt_token, type: :id_token)
77
+ new(jwt_token, raise_error: true, type: type).verify
64
78
  end
65
79
 
66
80
  attr_accessor :firebase_id_token_certificates
@@ -68,8 +82,9 @@ module FirebaseIdToken
68
82
  # Loads attributes: `:project_ids` from {FirebaseIdToken::Configuration},
69
83
  # and `:kid`, `:jwt_token` from the related `jwt_token`.
70
84
  # @param [String] jwt_token Firebase ID Token
71
- def initialize(jwt_token, raise_error: false)
85
+ def initialize(jwt_token, raise_error: false, type: :id_token)
72
86
  @raise_error = raise_error
87
+ @type = type
73
88
  @project_ids = FirebaseIdToken.configuration.project_ids
74
89
  @kid = extract_kid(jwt_token)
75
90
  @jwt_token = jwt_token
@@ -78,7 +93,8 @@ module FirebaseIdToken
78
93
 
79
94
  # @see Signature.verify
80
95
  def verify
81
- certificate = firebase_id_token_certificates.find(@kid, raise_error: @raise_error)
96
+ certificate = firebase_id_token_certificates.find(
97
+ @kid, raise_error: @raise_error, source: @type)
82
98
  return unless certificate
83
99
 
84
100
  payload = decode_jwt_payload(@jwt_token, certificate.public_key)
@@ -113,7 +129,7 @@ module FirebaseIdToken
113
129
  still_valid?(payload) &&
114
130
  @project_ids.include?(payload['aud']) &&
115
131
  issuer_authorized?(payload) &&
116
- ! payload['sub'].empty?
132
+ ! payload['sub'].to_s.empty?
117
133
  end
118
134
 
119
135
  def still_valid?(payload)
@@ -122,7 +138,8 @@ module FirebaseIdToken
122
138
  end
123
139
 
124
140
  def issuer_authorized?(payload)
125
- issuers = @project_ids.map { |i| "https://securetoken.google.com/#{i}" }
141
+ base_url = ISSUER_BASE_URLS.fetch(@type)
142
+ issuers = @project_ids.map { |i| "#{base_url}/#{i}" }
126
143
  issuers.include? payload['iss']
127
144
  end
128
145
  end
@@ -15,7 +15,7 @@ module FirebaseIdToken
15
15
  # `.find` is stubbed to always return the same certificate.
16
16
  # @param [String] kid Key ID
17
17
  # @return [nil, OpenSSL::X509::Certificate]
18
- def self.find(kid, raise_error: false)
18
+ def self.find(kid, raise_error: false, source: :id_token)
19
19
  cert = certificate
20
20
  OpenSSL::X509::Certificate.new cert
21
21
  end
@@ -1,3 +1,3 @@
1
1
  module FirebaseIdToken
2
- VERSION = '2.5.2'
2
+ VERSION = '4.0.0'
3
3
  end
@@ -1,15 +1,19 @@
1
- require 'redis'
2
- require 'redis-namespace'
3
1
  require 'httparty'
4
2
  require 'jwt'
3
+ require 'active_support'
4
+ require 'active_support/time'
5
+ require 'active_support/cache'
5
6
 
6
7
  require 'firebase_id_token/version'
7
8
  require 'firebase_id_token/exceptions/no_certificates_error'
8
9
  require 'firebase_id_token/exceptions/certificates_request_error'
9
10
  require 'firebase_id_token/exceptions/certificates_ttl_error'
11
+ require 'firebase_id_token/exceptions/unsupported_cache_operation_error'
10
12
  require 'firebase_id_token/exceptions/certificate_not_found'
11
13
  require 'firebase_id_token/configuration'
12
14
  require 'firebase_id_token/certificates'
15
+ require 'firebase_id_token/certificates/active_support'
16
+ require 'firebase_id_token/certificates/redis'
13
17
  require 'firebase_id_token/signature'
14
18
 
15
19
  # ## List of available methods
@@ -28,20 +32,18 @@ require 'firebase_id_token/signature'
28
32
  #
29
33
  # ## Configuration
30
34
  #
31
- # You need to set your Firebase Project ID. Additionally you can set your Redis
32
- # server instance in case you don't use Redis defaults.
35
+ # You need to set your Firebase Project ID and cache store.
33
36
  #
34
37
  # **WARNING:** Your `project_ids` must be a `Array`.
35
38
  # ```
36
39
  # FirebaseIdToken.configure do |config|
37
40
  # config.project_ids = ['my-project-id', 'another-project-id']
38
- # congig.redis = Redis.new(:host => "10.0.1.1", :port => 6380, :db => 15)
41
+ # congig.cache_store = ActiveSupport::Cache::RedisCacheStore.new
39
42
  # end
40
43
  # ```
41
44
  #
42
45
  # **Defaults**
43
46
  # + `project_ids` => `[]`
44
- # + `redis` => `Redis.new`
45
47
  #
46
48
  module FirebaseIdToken
47
49
  class << self
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'firebase_id_token/certificates/active_support'
3
+
4
+ module FirebaseIdToken
5
+ describe Certificates::ActiveSupport do
6
+ let (:certs) { File.read('spec/fixtures/files/certificates.json') }
7
+ let (:cache) { 'public, max-age=19302, must-revalidate, no-transform' }
8
+ let (:low_cache) { 'public, max-age=2160, must-revalidate, no-transform' }
9
+ let (:kid) { JSON.parse(certs).first[0] }
10
+ let (:expires_in) { (DateTime.now + (5/24r)).to_s }
11
+ let (:response) { double }
12
+
13
+ let (:mock_response) {
14
+ allow(response).to receive(:code) { 200 }
15
+ allow(response).to receive(:headers) { { 'cache-control' => cache } }
16
+ allow(response).to receive(:body) { certs }
17
+ }
18
+
19
+ let(:mock_request) {
20
+ mock_response
21
+ allow(HTTParty).to receive(:get).
22
+ with(an_instance_of(String)) { response }
23
+ }
24
+
25
+ context 'RedisCacheStore' do
26
+ before :each do
27
+ allow(FirebaseIdToken.configuration).to receive(:cache_store).and_return(
28
+ ActiveSupport::Cache::RedisCacheStore.new(namespace: 'firebase_id_token_test'))
29
+ FirebaseIdToken.configuration.cache_store.delete 'certificates'
30
+ end
31
+ it_behaves_like 'a certificate store', lazy: true
32
+ end
33
+
34
+ context 'MemoryStore' do
35
+ before :each do
36
+ allow(FirebaseIdToken.configuration).to receive(:cache_store).and_return(
37
+ ActiveSupport::Cache::MemoryStore.new(namespace: "firebase_auth"))
38
+ FirebaseIdToken.configuration.cache_store.delete 'certificates'
39
+ end
40
+ it_behaves_like 'a certificate store', lazy: true
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'redis'
3
+ require 'redis-namespace'
4
+ require 'firebase_id_token/certificates/redis'
5
+
6
+ module FirebaseIdToken
7
+ describe Certificates::Redis do
8
+ let (:redis) { Redis::Namespace.new 'firebase_id_token', redis: Redis.new }
9
+ let (:certs) { File.read('spec/fixtures/files/certificates.json') }
10
+ let (:cache) { 'public, max-age=19302, must-revalidate, no-transform' }
11
+ let (:low_cache) { 'public, max-age=2160, must-revalidate, no-transform' }
12
+ let (:kid) { JSON.parse(certs).first[0] }
13
+ let (:expires_in) { (DateTime.now + (5/24r)).to_s }
14
+ let (:response) { double }
15
+
16
+ let (:mock_response) {
17
+ allow(response).to receive(:code) { 200 }
18
+ allow(response).to receive(:headers) { { 'cache-control' => cache } }
19
+ allow(response).to receive(:body) { certs }
20
+ }
21
+
22
+ let(:mock_request) {
23
+ mock_response
24
+ allow(HTTParty).to receive(:get).
25
+ with(an_instance_of(String)) { response }
26
+ }
27
+
28
+ before :each do
29
+ FirebaseIdToken.configure { |config| config.redis = ::Redis.new }
30
+ redis.del 'certificates'
31
+ end
32
+
33
+ after :each do
34
+ FirebaseIdToken.reset
35
+ end
36
+
37
+ it_behaves_like 'a certificate store'
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ module FirebaseIdToken
4
+ describe 'Certificates with the session cookie source' do
5
+ let (:certs) { File.read('spec/fixtures/files/certificates.json') }
6
+ let (:cache) { 'public, max-age=19302, must-revalidate, no-transform' }
7
+ let (:kid) { JSON.parse(certs).first[0] }
8
+ let (:response) { double }
9
+
10
+ before :each do
11
+ allow(response).to receive(:code) { 200 }
12
+ allow(response).to receive(:headers) { { 'cache-control' => cache } }
13
+ allow(response).to receive(:body) { certs }
14
+ allow(HTTParty).to receive(:get).with(an_instance_of(String)) { response }
15
+ allow(FirebaseIdToken.configuration).to receive(:cache_store).and_return(
16
+ ActiveSupport::Cache::MemoryStore.new(namespace: 'firebase_auth'))
17
+ end
18
+
19
+ after :each do
20
+ FirebaseIdToken.reset
21
+ end
22
+
23
+ it 'requests the session cookie certificates URL' do
24
+ expect(HTTParty).to receive(:get).
25
+ with(Certificates::SESSION_COOKIE_URL).at_least(:once) { response }
26
+
27
+ Certificates.request!(source: :session_cookie)
28
+ end
29
+
30
+ it 'stores session cookie certificates apart from ID Token ones' do
31
+ Certificates.request!(source: :session_cookie)
32
+ cache_store = FirebaseIdToken.configuration.cache_store
33
+
34
+ expect(cache_store.read('session_cookie_certificates')).not_to be_nil
35
+ expect(cache_store.read('certificates')).to be_nil
36
+ end
37
+
38
+ it 'finds a certificate by kid on the session cookie source' do
39
+ expect(Certificates.find(kid, source: :session_cookie)).
40
+ to be_a(OpenSSL::X509::Certificate)
41
+ end
42
+
43
+ it 'reports the TTL of the session cookie source' do
44
+ Certificates.request!(source: :session_cookie)
45
+
46
+ expect(Certificates.ttl(source: :session_cookie)).to be > 3600
47
+ end
48
+ end
49
+ end
@@ -16,12 +16,6 @@ module FirebaseIdToken
16
16
  end
17
17
  end
18
18
 
19
- describe '.redis' do
20
- it 'sets a Redis instance as default' do
21
- expect(Configuration.new.redis).to be_a(Redis)
22
- end
23
- end
24
-
25
19
  describe '.redis=' do
26
20
  it 'changes default values' do
27
21
  config = Configuration.new
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ module FirebaseIdToken
4
+ describe Signature do
5
+ describe 'verifying a Session Cookie' do
6
+ let(:jwt) { JSON.parse File.read('spec/fixtures/files/jwt.json') }
7
+ let(:rsa_private) { OpenSSL::PKey::RSA.new(jwt['private_key']) }
8
+
9
+ let(:session_cookie_payload) do
10
+ {
11
+ 'iss' => 'https://session.firebase.google.com/firebase-id-token',
12
+ 'aud' => 'firebase-id-token',
13
+ 'sub' => 'user-123',
14
+ 'iat' => Time.current.to_i - 10,
15
+ 'exp' => Time.current.to_i + 3600
16
+ }
17
+ end
18
+
19
+ let(:session_cookie) do
20
+ JWT.encode session_cookie_payload, rsa_private, 'RS256', kid: 'test'
21
+ end
22
+
23
+ let(:mock_certificates) do
24
+ allow(Certificates)
25
+ .to(receive(:find))
26
+ .with(an_instance_of(String), raise_error: false, source: anything)
27
+ .and_return(OpenSSL::X509::Certificate.new(jwt['certificate']))
28
+ end
29
+
30
+ before :each do
31
+ mock_certificates
32
+ FirebaseIdToken.configure do |config|
33
+ config.project_ids = ['firebase-id-token']
34
+ end
35
+ end
36
+
37
+ after :each do
38
+ FirebaseIdToken.reset
39
+ end
40
+
41
+ it 'returns the payload with type: :session_cookie' do
42
+ expect(described_class.verify(session_cookie, type: :session_cookie)).
43
+ to be_a(Hash)
44
+ end
45
+
46
+ it 'looks up certificates on the session cookie source' do
47
+ expect(Certificates).to receive(:find).
48
+ with(an_instance_of(String), raise_error: false,
49
+ source: :session_cookie).
50
+ and_return(OpenSSL::X509::Certificate.new(jwt['certificate']))
51
+
52
+ described_class.verify(session_cookie, type: :session_cookie)
53
+ end
54
+
55
+ it 'returns nil when verifying a Session Cookie as an ID Token' do
56
+ expect(described_class.verify(session_cookie)).to be(nil)
57
+ end
58
+
59
+ it 'returns nil when verifying an ID Token as a Session Cookie' do
60
+ expect(described_class.verify(jwt['jwt_token'], type: :session_cookie)).
61
+ to be(nil)
62
+ end
63
+ end
64
+ end
65
+ end