rack-firebase 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +81 -0
- data/lib/rack/firebase/authorization_header.rb +15 -0
- data/lib/rack/firebase/configuration.rb +15 -0
- data/lib/rack/firebase/error.rb +6 -0
- data/lib/rack/firebase/middleware.rb +95 -0
- data/lib/rack/firebase/version.rb +5 -0
- data/lib/rack/firebase.rb +21 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3dcb76b9dd459436772a9058c79c1fda81de49e4b1ce0c76e74e6611a96718d3
|
4
|
+
data.tar.gz: 9e15447178511fe28a6e367ae2a1ce97fd7c4664d416cca36a507b90055a3e6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9786153f3cc1e21470bf30df88b1cb06c54dbf4f5f3f1e12d738dee189e373da35b2a98e3f8e03ca558cbd33735a7e828f25cdaf529f87c874991b94d6702d97
|
7
|
+
data.tar.gz: 910c6ab79bf38b4634278f5710ba42de25a9f81bba5bd3a8590bfcd8671a6a3e30b157ea5e9db974245c87119e774799132034a5c4a14c289a7564fafc0e5dff
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2022 Laura Mosher
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Rack Firebase Middleware
|
2
|
+
|
3
|
+
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md)
|
4
|
+
|
5
|
+
|
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).
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add the gem to your Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "rack-firebase"
|
14
|
+
```
|
15
|
+
|
16
|
+
And execute
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or, install it yourself:
|
21
|
+
|
22
|
+
$ gem install rack-firebase
|
23
|
+
|
24
|
+
## Configuration
|
25
|
+
|
26
|
+
Configure your Firebase Project ID(s):
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
Rack::Firebase.configure do |config|
|
30
|
+
config.project_ids = ["your-project-id"]
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
Add the middleware to your rack application:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
use Rack::Firebase::Middleware
|
38
|
+
```
|
39
|
+
|
40
|
+
Now, all incoming requests will require a verified Firebase token.
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
While just adding the middleware to your app will *block* requests without a verified token, your app will still need to handle the connection between the token and the subject in your application.
|
45
|
+
|
46
|
+
In order to facilitate this, the Subject claim (Device or User UID) is added to the request env before yielding to your app layer.
|
47
|
+
|
48
|
+
For example, assuming your User's model has a `uid` attribute for storing Firebase UID's:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
def current_user
|
52
|
+
return @current_user if defined? @current_user
|
53
|
+
|
54
|
+
uid = request.env[Rack::Firebase::Middleware::USER_UID]
|
55
|
+
if uid
|
56
|
+
@current_user = User.find_by(uid: uid)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def user_signed_in?
|
61
|
+
current_user.present?
|
62
|
+
end
|
63
|
+
|
64
|
+
def authenticate_user!
|
65
|
+
unless user_signed_in?
|
66
|
+
# deny access and abort request from application layer.
|
67
|
+
end
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
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
|
+
|
73
|
+
## Contributing
|
74
|
+
|
75
|
+
Bug reports and pull requests are welcome on GitHub.
|
76
|
+
|
77
|
+
This project is intended to be a safe, welcoming space for collaboration. All contributors are expected to adhere to the [Contributor Covenant](https://www.contributor-covenant.org) Code of Conduct.
|
78
|
+
|
79
|
+
## License
|
80
|
+
|
81
|
+
This gem is available as open source under the terms of the [MIT License](LICENSE).
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rack
|
2
|
+
module Firebase
|
3
|
+
class AuthorizationHeader
|
4
|
+
METHOD = "Bearer"
|
5
|
+
AUTH_HEADER_ENV = "HTTP_AUTHORIZATION"
|
6
|
+
|
7
|
+
def self.read_token(env)
|
8
|
+
return unless (auth = env[AUTH_HEADER_ENV])
|
9
|
+
|
10
|
+
method, token = auth.split
|
11
|
+
return token if method == METHOD
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "jwt"
|
2
|
+
require "net/http"
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Firebase
|
6
|
+
class Middleware
|
7
|
+
ALG = "RS256".freeze
|
8
|
+
CERTIFICATE_URL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com".freeze
|
9
|
+
USER_UID = "firebase.user.uid"
|
10
|
+
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
|
14
|
+
@config = ::Rack::Firebase.configuration
|
15
|
+
|
16
|
+
@jwt_loader = FIREBASE_KEY_LOADER
|
17
|
+
@error_responder = DEFAULT_ERROR_RESPONDER
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
token = AuthorizationHeader.read_token(env)
|
22
|
+
decoded_token, _ = JWT.decode(
|
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
|
+
)
|
32
|
+
|
33
|
+
raise Rack::Firebase::InvalidSubError.new("Invalid subject") if decoded_token["sub"].nil? || decoded_token["sub"] == ""
|
34
|
+
raise Rack::Firebase::InvalidAuthTimeError.new("Invalid auth time") unless decoded_token["auth_time"] <= Time.now.to_i
|
35
|
+
|
36
|
+
env[USER_UID] = decoded_token["sub"]
|
37
|
+
@app.call(env)
|
38
|
+
rescue JWT::JWKError => error # Issues with fetched JWKs
|
39
|
+
error_responder.call(error, "unauthorized")
|
40
|
+
rescue JWT::ExpiredSignature => error # Token has expired
|
41
|
+
error_responder.call(error, "expired")
|
42
|
+
rescue JWT::InvalidIatError => error # invalid issued at claim (iat)
|
43
|
+
error_responder.call(error, "unauthorized")
|
44
|
+
rescue JWT::InvalidIssuerError => error # invalid issuer
|
45
|
+
error_responder.call(error, "unauthorized")
|
46
|
+
rescue JWT::InvalidAudError => error # invalid audience
|
47
|
+
error_responder.call(error, "unauthorized")
|
48
|
+
rescue JWT::DecodeError => error # General JWT error
|
49
|
+
error_responder.call(error, "unauthorized")
|
50
|
+
rescue Rack::Firebase::InvalidSubError => error # subject is empty or missing
|
51
|
+
error_responder.call(error, "unauthorized")
|
52
|
+
rescue Rack::Firebase::InvalidAuthTimeError => error # auth time is in the future
|
53
|
+
error_responder.call(error, "unauthorized")
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
attr_reader :config, :error_responder, :jwt_loader
|
59
|
+
|
60
|
+
DEFAULT_ERROR_RESPONDER = lambda do |error, reason|
|
61
|
+
error_detail = {
|
62
|
+
error: error.message,
|
63
|
+
message: reason
|
64
|
+
}.to_json
|
65
|
+
[401, {"content-type" => "application/json"}, [error_detail]]
|
66
|
+
end
|
67
|
+
|
68
|
+
FIREBASE_KEY_LOADER = lambda do |options|
|
69
|
+
if options[:kid_not_found] || (@refresh_cache_by.to_i < Time.now.to_i + 3600)
|
70
|
+
@cached_keys = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
@cached_keys ||= begin
|
74
|
+
response = ::Net::HTTP.get_response(URI(CERTIFICATE_URL))
|
75
|
+
cache_control = response["Cache-Control"]
|
76
|
+
|
77
|
+
expires_in = cache_control.match(/max-age=([0-9]+)/).captures.first.to_i
|
78
|
+
@refresh_cache_by = Time.now.to_i + expires_in
|
79
|
+
|
80
|
+
json = JSON.parse(response.body)
|
81
|
+
json.map do |kid, cert_string|
|
82
|
+
key = OpenSSL::X509::Certificate.new(cert_string).public_key
|
83
|
+
JWT::JWK::RSA.new(key, kid: kid)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def firebase_issuers
|
89
|
+
config.project_ids.map { |project_id|
|
90
|
+
"https://securetoken.google.com/#{project_id}"
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "rack/firebase/configuration"
|
2
|
+
require "rack/firebase/version"
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Firebase
|
6
|
+
class << self
|
7
|
+
attr_writer :configuration
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.configuration
|
11
|
+
@configuration ||= Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
yield configuration
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
require "rack/firebase/error"
|
20
|
+
require "rack/firebase/authorization_header"
|
21
|
+
require "rack/firebase/middleware"
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-firebase
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Laura Mosher
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-12-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jwt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: openssl
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.14'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.14'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.0.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.0.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.22.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.22.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: standard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.9.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.9.0
|
111
|
+
description: A simple, lightweight Rack middleware to verify Firebase tokens.
|
112
|
+
email: laura@mosher.tech
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- LICENSE
|
118
|
+
- README.md
|
119
|
+
- lib/rack/firebase.rb
|
120
|
+
- lib/rack/firebase/authorization_header.rb
|
121
|
+
- lib/rack/firebase/configuration.rb
|
122
|
+
- lib/rack/firebase/error.rb
|
123
|
+
- lib/rack/firebase/middleware.rb
|
124
|
+
- lib/rack/firebase/version.rb
|
125
|
+
homepage: https://github.com/lauramosher/rack-firebase
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata: {}
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubygems_version: 3.1.2
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: Verify Firebase ID Tokens in Middleware
|
148
|
+
test_files: []
|