rack-firebase 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +80 -1
- data/lib/rack/firebase/error.rb +1 -0
- data/lib/rack/firebase/middleware.rb +1 -12
- data/lib/rack/firebase/test_helpers.rb +57 -0
- data/lib/rack/firebase/token_decoder.rb +35 -0
- data/lib/rack/firebase/version.rb +1 -1
- data/lib/rack/firebase.rb +23 -0
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a4081b1727087c19ec8761fafd9d0507f4b398327b3a5458e663569d3381ed8
|
4
|
+
data.tar.gz: 4196e0601c2552042edac107ff91d1a08cb7df6b66d00b83022184d88da0526e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41523e3d39d08a86d760b36159a70f4e27ef4c14eb015e2c294e7634995bf6cbbd13a139ef11bdf1b976f305cc12d8cdaefa7990ea9adfe1740766fe73553668
|
7
|
+
data.tar.gz: 275486512df3b419d95a9dca6b9d0d032f77a768a8ff54b5c84e5dc52ebafbcd9df95adbe8dd3e8695b84b9c072618366e85928516169a48bd110644fc193e53
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Rack Firebase Middleware
|
2
2
|
|
3
|
-
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md)
|
3
|
+
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md) [![Gem Version](https://badge.fury.io/rb/rack-firebase.svg)](https://rubygems.org/gems/rack-firebase)
|
4
4
|
|
5
5
|
|
6
6
|
A rack middleware for verifying ID tokens from Google's Firebase. It provides token decoding and verification using [Firebase's 3rd Party Verification constraints](https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=en&authuser=3#verify_id_tokens_using_a_third-party_jwt_library).
|
@@ -70,6 +70,85 @@ end
|
|
70
70
|
|
71
71
|
From here, you can invoke `authenticate_user!` to ensure the token subject is actually a user in your application and use `current_user` to scope your requests or handle more granular authorization.
|
72
72
|
|
73
|
+
### Testing
|
74
|
+
|
75
|
+
In order to test your authenticated routes, you'll need to provide a valid token in the authorization header.
|
76
|
+
|
77
|
+
Since Firebase typically issues signed tokens using their certificates, this can make it difficult to test your authenticated routes with valid tokens.
|
78
|
+
|
79
|
+
As such, some test helpers are provided to help faciliate automated testing.
|
80
|
+
|
81
|
+
> Note: For manual testing, it is recommended to create a test-specific user in your Firebase project and test the full authentication flow with your routes.
|
82
|
+
|
83
|
+
#### Mocking requests for public keys
|
84
|
+
|
85
|
+
There are two options for mocking the requests from Google; choose the one that best fits your testing library and needs:
|
86
|
+
|
87
|
+
1. Explicitly start and stop firebase mocks, or
|
88
|
+
2. Wrap each example that needs to be mocked.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# Require the test helpers
|
92
|
+
require "rack/firebase/test_helpers"
|
93
|
+
|
94
|
+
describe "your request specs" do
|
95
|
+
# Explicitly mock before/after
|
96
|
+
before { Rack::Firebase::TestHelpers.mock_start }
|
97
|
+
after { Rack::Firebase::TestHelpers.mock_end }
|
98
|
+
|
99
|
+
# Or wrap each example
|
100
|
+
# This is for RSpec only! Use setup/teardown or before/after with the explicit mock for Minitest.
|
101
|
+
around(:example) do |example|
|
102
|
+
Rack::Firebase::TestHelpers.mock_signature_verification do
|
103
|
+
example.run
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Optionally, you can pass minutes as a number to `mock_start` to overwrite when the cache key should be refreshed. By default, this is set to `5000`, or approximately 83 minutes.
|
110
|
+
|
111
|
+
#### Generating Tokens
|
112
|
+
|
113
|
+
A test helper is provided that will generate a valid token and add it to your request headers for your specs.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
# Require the test helpers
|
117
|
+
require "rack/firebase/test_helpers"
|
118
|
+
|
119
|
+
it "tests something" do
|
120
|
+
user = fetch_user() # this presumes your user has a `uid`!
|
121
|
+
headers = { "Accept" => "application/json", "Content-Type" => "application/json" }
|
122
|
+
|
123
|
+
# This will generate a valid token and add it to your headers
|
124
|
+
auth_headers = Rack::Firebase::TestHelpers.auth_headers(headers, user.uid)
|
125
|
+
|
126
|
+
get "/", headers: auth_headers
|
127
|
+
|
128
|
+
expect(last_response).to be_ok
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
##### Customizing
|
133
|
+
|
134
|
+
By default, the token will be created using the first project ID provided in your configuration. If your app is configured for multiple projects and you wish to test one of the other projects, you can optionally add the `aud` to the arguments:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
Rack::Firebase::TestHelpers.auth_headers(headers, user.uid, aud: "different-project")
|
138
|
+
```
|
139
|
+
|
140
|
+
There is a fourth arg that takes a hash of options that will let you alter the payload.
|
141
|
+
|
142
|
+
> Caution: it is possible for provided options to produce invalid tokens.
|
143
|
+
|
144
|
+
| argument | description | default |
|
145
|
+
| --- | --- | --- |
|
146
|
+
| email | User's email address used for authentication. Added to the payload as `email` and included in the array of email identities in `firebase.identities.email` | test@test.com |
|
147
|
+
| verified | Denotes if the email used to sign in is verified by the user | false |
|
148
|
+
| auth_time | The last authentication time recorded by Google | Current time |
|
149
|
+
| iat | Denotes the time the token was issued. When iat is provided, but not an auth time, the auth time is also set to the iat time. | Current time |
|
150
|
+
| exp | Denotes when the token expires | Current time + 5000 |
|
151
|
+
|
73
152
|
## Contributing
|
74
153
|
|
75
154
|
Bug reports and pull requests are welcome on GitHub.
|
data/lib/rack/firebase/error.rb
CHANGED
@@ -4,8 +4,6 @@ require "net/http"
|
|
4
4
|
module Rack
|
5
5
|
module Firebase
|
6
6
|
class Middleware
|
7
|
-
ALG = "RS256".freeze
|
8
|
-
CERTIFICATE_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com".freeze
|
9
7
|
USER_UID = "firebase.user.uid"
|
10
8
|
|
11
9
|
def initialize(app)
|
@@ -19,16 +17,7 @@ module Rack
|
|
19
17
|
|
20
18
|
def call(env)
|
21
19
|
token = AuthorizationHeader.read_token(env)
|
22
|
-
decoded_token
|
23
|
-
token, nil, true,
|
24
|
-
{
|
25
|
-
jwks: jwt_loader,
|
26
|
-
algorithm: ALG,
|
27
|
-
verify_iat: true,
|
28
|
-
verify_aud: true, aud: config.project_ids,
|
29
|
-
verify_iss: true, iss: firebase_issuers
|
30
|
-
}
|
31
|
-
)
|
20
|
+
decoded_token = TokenDecoder.new.call(token)
|
32
21
|
|
33
22
|
raise Rack::Firebase::InvalidSubError.new("Invalid subject") if decoded_token["sub"].nil? || decoded_token["sub"] == ""
|
34
23
|
raise Rack::Firebase::InvalidAuthTimeError.new("Invalid auth time") unless decoded_token["auth_time"] <= Time.now.to_i
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rack
|
2
|
+
module Firebase
|
3
|
+
module TestHelpers
|
4
|
+
def self.auth_headers(headers, uid, aud: nil, options: {})
|
5
|
+
cached_current_time = Time.now.to_i
|
6
|
+
aud ||= Rack::Firebase.configuration.project_ids.first
|
7
|
+
|
8
|
+
payload = {
|
9
|
+
iss: "https://securetoken.google.com/#{aud}",
|
10
|
+
aud: aud,
|
11
|
+
auth_time: options[:auth_time] || options[:iat] || cached_current_time,
|
12
|
+
user_id: uid,
|
13
|
+
sub: uid,
|
14
|
+
iat: options[:iat] || cached_current_time,
|
15
|
+
exp: options[:exp] || cached_current_time + 5000,
|
16
|
+
email: options[:email] || "test@test.com",
|
17
|
+
email_verified: !!options[:verified],
|
18
|
+
firebase: {
|
19
|
+
identities: {
|
20
|
+
email: [
|
21
|
+
options[:email],
|
22
|
+
"test@test.com"
|
23
|
+
]
|
24
|
+
},
|
25
|
+
sign_in_provider: "password"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
token = JWT.encode(payload, secret_key, Rack::Firebase::ALG, kid: "1234567890")
|
29
|
+
headers = headers.dup
|
30
|
+
headers["Authorization"] = "Bearer #{token}"
|
31
|
+
headers
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.mock_start(expires_in = 5000)
|
35
|
+
Rack::Firebase.instance_variable_set(:@cached_keys, [JWT::JWK::RSA.new(secret_key.public_key, kid: "1234567890")])
|
36
|
+
Rack::Firebase.instance_variable_set(:@refresh_cache_by, Time.now.to_i + expires_in)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.mock_signature_verification
|
40
|
+
mock_start
|
41
|
+
yield
|
42
|
+
mock_end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.mock_end
|
46
|
+
Rack::Firebase.instance_variable_set(:@cached_keys, nil)
|
47
|
+
Rack::Firebase.instance_variable_set(:@refresh_cache_by, nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
private_class_method
|
51
|
+
|
52
|
+
def self.secret_key
|
53
|
+
@secret_key ||= OpenSSL::PKey::RSA.generate(2048)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "jwt"
|
2
|
+
require "net/http"
|
3
|
+
require "rack/firebase"
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Firebase
|
7
|
+
class TokenDecoder
|
8
|
+
attr_accessor :config, :jwt_loader
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@jwt_loader = FIREBASE_KEY_LOADER
|
12
|
+
@config = Rack::Firebase.configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(token)
|
16
|
+
JWT.decode(
|
17
|
+
token, nil, true,
|
18
|
+
{
|
19
|
+
jwks: jwt_loader,
|
20
|
+
algorithm: ALG,
|
21
|
+
verify_iat: true,
|
22
|
+
verify_aud: true, aud: config.project_ids,
|
23
|
+
verify_iss: true, iss: firebase_issuers
|
24
|
+
}
|
25
|
+
)[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
def firebase_issuers
|
29
|
+
config.project_ids.map { |project_id|
|
30
|
+
"https://securetoken.google.com/#{project_id}"
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rack/firebase.rb
CHANGED
@@ -14,8 +14,31 @@ module Rack
|
|
14
14
|
def self.configure
|
15
15
|
yield configuration
|
16
16
|
end
|
17
|
+
|
18
|
+
ALG = "RS256".freeze
|
19
|
+
CERTIFICATE_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com".freeze
|
20
|
+
FIREBASE_KEY_LOADER = lambda do |options|
|
21
|
+
if options[:kid_not_found] || (@refresh_cache_by.to_i < Time.now.to_i + 3600)
|
22
|
+
@cached_keys = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
@cached_keys ||= begin
|
26
|
+
response = ::Net::HTTP.get_response(URI(CERTIFICATE_URL))
|
27
|
+
cache_control = response["Cache-Control"]
|
28
|
+
|
29
|
+
expires_in = cache_control.match(/max-age=([0-9]+)/).captures.first.to_i
|
30
|
+
@refresh_cache_by = Time.now.to_i + expires_in
|
31
|
+
|
32
|
+
json = JSON.parse(response.body)
|
33
|
+
json.map do |kid, cert_string|
|
34
|
+
key = OpenSSL::X509::Certificate.new(cert_string).public_key
|
35
|
+
JWT::JWK::RSA.new(key, kid: kid)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
17
39
|
end
|
18
40
|
end
|
19
41
|
require "rack/firebase/error"
|
20
42
|
require "rack/firebase/authorization_header"
|
43
|
+
require "rack/firebase/token_decoder"
|
21
44
|
require "rack/firebase/middleware"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-firebase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Laura Mosher
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '3.12'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '3.12'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rack-test
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 1.
|
103
|
+
version: 1.20.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 1.
|
110
|
+
version: 1.20.0
|
111
111
|
description: A simple, lightweight Rack middleware to verify Firebase tokens.
|
112
112
|
email: laura@mosher.tech
|
113
113
|
executables: []
|
@@ -121,6 +121,8 @@ files:
|
|
121
121
|
- lib/rack/firebase/configuration.rb
|
122
122
|
- lib/rack/firebase/error.rb
|
123
123
|
- lib/rack/firebase/middleware.rb
|
124
|
+
- lib/rack/firebase/test_helpers.rb
|
125
|
+
- lib/rack/firebase/token_decoder.rb
|
124
126
|
- lib/rack/firebase/version.rb
|
125
127
|
homepage: https://github.com/lauramosher/rack-firebase
|
126
128
|
licenses:
|
@@ -141,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
143
|
- !ruby/object:Gem::Version
|
142
144
|
version: '0'
|
143
145
|
requirements: []
|
144
|
-
rubygems_version: 3.1
|
146
|
+
rubygems_version: 3.4.1
|
145
147
|
signing_key:
|
146
148
|
specification_version: 4
|
147
149
|
summary: Verify Firebase ID Tokens in Middleware
|