jwt_signed_request 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 753794ac95f697ec022de56fe9d4db85265e4920
4
+ data.tar.gz: 16109643b05330e94cd586f246e57039892fa2ee
5
+ SHA512:
6
+ metadata.gz: 85f8a16181085937c113444c211d7e0eba0ecfe865d6e829576589713825df3263f392f44a4edb08ccf2ad3986bcf702964622ed9f8e69dc28f56d85a0f83283
7
+ data.tar.gz: dbbd9e2db0bfe7c0ca0c53f233af500f02aa878dfdf52edefe8408b7629b18d11da4a05016873e6361a96fb1626175e187410523e0954c9bcd558a3197bad9ff
data/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # JWT Signed Request
2
+
3
+ Request signing and verification for Internal APIs using JWT.
4
+
5
+ ## Getting Started
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'jwt_signed_request'
11
+ ```
12
+
13
+ then run:
14
+
15
+ ```sh
16
+ $ bundle
17
+ ```
18
+
19
+ ## Generating EC Keys
20
+
21
+ We should be using a public key encryption alogorithm such as **ES256**. To generate your public/private key pair using **ES256** run:
22
+
23
+ ```sh
24
+ $ openssl ecparam -genkey -name prime256v1 -noout -out myprivatekey.pem
25
+ $ openssl ec -in myprivatekey.pem -pubout -out mypubkey.pem
26
+ ```
27
+
28
+ Store and encrypt these in your application secrets.
29
+
30
+ ## Signing Requests
31
+
32
+ If you are using an asymmetrical encryption algorithm such as ES256 you will sign your requests using the private key.
33
+
34
+ ### Using net/http
35
+
36
+ ```ruby
37
+ require 'net/http'
38
+ require 'uri'
39
+ require 'openssl'
40
+ require 'jwt_signed_request'
41
+
42
+ private_key = """
43
+ -----BEGIN EC PRIVATE KEY-----
44
+ MHcCAQEEIBOQ3YIILYMV1glTKbF9oeZWzHe3SNQjAx4IbPIxNygQoAoGCCqGSM49
45
+ AwEHoUQDQgAEuOC3ufTTnW0hVmCPNERb4LxaDE/OexDdlmXEjHYaixzYIduluGXd
46
+ 3cjg4H2gjqsY/NCpJ9nM8/AAINSrq+qPuA==
47
+ -----END EC PRIVATE KEY-----
48
+ """
49
+
50
+ uri = URI('http://example.com')
51
+ req = Net::HTTP::Get.new(uri)
52
+
53
+ req['Authorization'] = JWTSignedRequest.sign(
54
+ method: req.method,
55
+ path: req.path,
56
+ headers: {"Content-Type" => "application/json"},
57
+ body: "",
58
+ secret_key: OpenSSL::PKey::EC.new(private_key),
59
+ algorithm: 'ES256', # optional (default: ES256)
60
+ key_id: 'my-key-id', # optional
61
+ issuer: 'my-issuer' # optional
62
+ additional_headers_to_sign: ['X-AUTH'] # optional
63
+ )
64
+
65
+ res = Net::HTTP.start(uri.hostname, uri.port) {|http|
66
+ http.request(req)
67
+ }
68
+ ```
69
+
70
+ ### Using faraday
71
+
72
+ ```ruby
73
+ require 'faraday'
74
+ require 'openssl'
75
+ require 'jwt_signed_request/middlewares/faraday'
76
+
77
+ private_key = """
78
+ -----BEGIN EC PRIVATE KEY-----
79
+ MHcCAQEEIBOQ3YIILYMV1glTKbF9oeZWzHe3SNQjAx4IbPIxNygQoAoGCCqGSM49
80
+ AwEHoUQDQgAEuOC3ufTTnW0hVmCPNERb4LxaDE/OexDdlmXEjHYaixzYIduluGXd
81
+ 3cjg4H2gjqsY/NCpJ9nM8/AAINSrq+qPuA==
82
+ -----END EC PRIVATE KEY-----
83
+ """
84
+
85
+ conn = Faraday.new(url: URI.parse('http://example.com')) do |faraday|
86
+ faraday.use JWTSignedRequest::Middlewares::Faraday,
87
+ secret_key: OpenSSL::PKey::EC.new(private_key),
88
+ algorithm: 'EC256', # optional (default: ES256)
89
+ key_id: 'my-key-id', # optional
90
+ issuer: 'my-issuer', # optional
91
+ additional_headers_to_sign: ['X-AUTH'] # optional
92
+
93
+ faraday.adapter Faraday.default_adapter
94
+ end
95
+
96
+ conn.post do |req|
97
+ req.url 'http://example.com'
98
+ req.body = '{ "name": "Unagi" }'
99
+ end
100
+ ```
101
+
102
+ ## Verifying Requests
103
+
104
+ If you are using an asymmetrical encryption algorithm such as ES256 you will verify the request using the public key.
105
+
106
+ ## Using Rails
107
+
108
+ ```ruby
109
+ class APIController < ApplicationController
110
+ PUBLIC_KEY = """
111
+ -----BEGIN PUBLIC KEY-----
112
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuOC3ufTTnW0hVmCPNERb4LxaDE/O
113
+ exDdlmXEjHYaixzYIduluGXd3cjg4H2gjqsY/NCpJ9nM8/AAINSrq+qPuA==
114
+ -----END PUBLIC KEY-----
115
+ """
116
+
117
+ before_action :verify_request
118
+
119
+ ...
120
+
121
+ private
122
+
123
+ def verify_request
124
+ begin
125
+ JWTSignedRequest.verify(
126
+ request: request,
127
+ secret_key: OpenSSL::PKey::EC.new(PUBLIC_KEY)
128
+ )
129
+
130
+ rescue JWTSignedRequest::UnauthorizedRequestError => e
131
+ render :json => {}, :status => :unauthorized
132
+ end
133
+ end
134
+
135
+ end
136
+ ```
137
+
138
+ ## Using Rack Middleware
139
+
140
+ ```ruby
141
+ PUBLIC_KEY = """
142
+ -----BEGIN PUBLIC KEY-----
143
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuOC3ufTTnW0hVmCPNERb4LxaDE/O
144
+ exDdlmXEjHYaixzYIduluGXd3cjg4H2gjqsY/NCpJ9nM8/AAINSrq+qPuA==
145
+ -----END PUBLIC KEY-----
146
+ """
147
+
148
+ class Server < Sinatra::Base
149
+ use JWTSignedRequest::Middlewares::Rack
150
+ secret_key: OpenSSL::PKey::EC.new(PUBLIC_KEY)
151
+ end
152
+ ```
153
+
154
+ ## Maintainers
155
+ - [Toan Nguyen](https://github.com/yoshdog)
156
+
157
+ ## License
158
+
159
+ `JWTSignedRequest` uses MIT license. See
160
+ [`LICENSE.txt`](https://github.com/envato/jwt_signed_request/blob/master/LICENSE.txt) for
161
+ details.
162
+
163
+ ## Code of conduct
164
+
165
+ We welcome contribution from everyone. Read more about it in
166
+ [`CODE_OF_CONDUCT.md`](https://github.com/envato/jwt_signed_request/blob/master/CODE_OF_CONDUCT.md)
167
+
168
+ ## Contributing
169
+
170
+ For bug fixes, documentation changes, and small features:
171
+
172
+ 1. Fork it ( https://github.com/[my-github-username]/jwt_signed_request/fork )
173
+ 2. Create your feature branch (git checkout -b my-new-feature)
174
+ 3. Commit your changes (git commit -am 'Add some feature')
175
+ 4. Push to the branch (git push origin my-new-feature)
176
+ 5. Create a new Pull Request
177
+
178
+ For larger new features: Do everything as above, but first also make contact with the project maintainers to be sure your change fits with the project direction and you won't be wasting effort going in the wrong direction
@@ -0,0 +1,81 @@
1
+ require 'digest'
2
+ require 'json'
3
+ require 'rack/utils'
4
+
5
+ module JWTSignedRequest
6
+ class Claims
7
+ EMPTY_HEADERS = [].freeze
8
+
9
+ def self.generate(args)
10
+ new(**args).generate
11
+ end
12
+
13
+ def initialize(method:, path:, headers:, body:, additional_headers_to_sign: EMPTY_HEADERS, timeout: DEFAULT_TIMEOUT, issuer:)
14
+ @method = method
15
+ @path = path
16
+ @headers = headers
17
+ @body = body
18
+ @additional_headers_to_sign = additional_headers_to_sign
19
+ @timeout = timeout
20
+ @issuer = issuer
21
+ end
22
+
23
+ private_class_method :new
24
+
25
+ def generate
26
+ result = {
27
+ method: method,
28
+ path: path,
29
+ headers: serialized_headers,
30
+ body_sha: body_sha,
31
+ exp: (Time.now + timeout).to_i
32
+ }
33
+ result[:iss] = issuer if issuer
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :method, :path, :headers, :body, :additional_headers_to_sign, :timeout, :issuer
40
+
41
+ HEADERS_TO_SIGN = %w(
42
+ Content-Type
43
+ Content-Length
44
+ Date
45
+ User-Agent
46
+ ).freeze
47
+
48
+ private_constant :HEADERS_TO_SIGN
49
+
50
+ DEFAULT_TIMEOUT = 30.freeze
51
+
52
+ private_constant :DEFAULT_TIMEOUT
53
+
54
+ def formatted_body
55
+ case body
56
+ when String
57
+ body
58
+ when Array, Hash
59
+ Rack::Utils.build_query(body)
60
+ end
61
+ end
62
+
63
+ def body_sha
64
+ Digest::SHA256.hexdigest(formatted_body)
65
+ end
66
+
67
+ def headers_to_sign
68
+ HEADERS_TO_SIGN + additional_headers_to_sign
69
+ end
70
+
71
+ def filtered_headers
72
+ headers.select do |header_name, _|
73
+ headers_to_sign.include?(header_name)
74
+ end
75
+ end
76
+
77
+ def serialized_headers
78
+ JSON.dump(filtered_headers)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,68 @@
1
+ # We need a way to pull out the headers from a RAW Rack ENV hash.
2
+ #
3
+ # We took out the bits we need to lookup the headers from:
4
+ # https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/headers.rb
5
+ #
6
+ # We didn't want to include actionpack as a dependency of the library as it brings in alot of
7
+ # other dependencies.
8
+
9
+ module JWTSignedRequest
10
+ class Headers
11
+ def self.fetch(key, request)
12
+ new(request).fetch(key)
13
+ end
14
+
15
+ def initialize(request)
16
+ @request = request
17
+ end
18
+
19
+ def fetch(key)
20
+ env_key = env_name(key)
21
+ request_env[env_key]
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :request
27
+
28
+ def request_env
29
+ request.env
30
+ end
31
+
32
+ CGI_VARIABLES = Set.new(%W[
33
+ AUTH_TYPE
34
+ CONTENT_LENGTH
35
+ CONTENT_TYPE
36
+ GATEWAY_INTERFACE
37
+ HTTPS
38
+ PATH_INFO
39
+ PATH_TRANSLATED
40
+ QUERY_STRING
41
+ REMOTE_ADDR
42
+ REMOTE_HOST
43
+ REMOTE_IDENT
44
+ REMOTE_USER
45
+ REQUEST_METHOD
46
+ SCRIPT_NAME
47
+ SERVER_NAME
48
+ SERVER_PORT
49
+ SERVER_PROTOCOL
50
+ SERVER_SOFTWARE
51
+ ]).freeze
52
+
53
+ private_constant :CGI_VARIABLES
54
+
55
+ HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
56
+
57
+ private_constant :HTTP_HEADER
58
+
59
+ def env_name(key)
60
+ key = key.to_s
61
+ if key =~ HTTP_HEADER
62
+ key = key.upcase.tr('-', '_')
63
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
64
+ end
65
+ key
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ require 'faraday'
2
+ require 'jwt_signed_request'
3
+
4
+ module JWTSignedRequest
5
+ module Middlewares
6
+ class Faraday < Faraday::Middleware
7
+ def initialize(app, options)
8
+ @options = options
9
+ super(app)
10
+ end
11
+
12
+ def call(env)
13
+ jwt_token = ::JWTSignedRequest.sign(
14
+ method: env[:method],
15
+ path: env[:url].request_uri,
16
+ headers: env[:request_headers],
17
+ body: env.fetch(:body, ::JWTSignedRequest::EMPTY_BODY),
18
+ secret_key: options[:secret_key],
19
+ **optional_settings
20
+ )
21
+
22
+ env[:request_headers].store("Authorization", jwt_token)
23
+ app.call(env)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :app, :env, :options
29
+
30
+ def optional_settings
31
+ {
32
+ algorithm: options[:algorithm],
33
+ additional_headers_to_sign: options[:additional_headers_to_sign],
34
+ key_id: options[:key_id],
35
+ issuer: options[:issuer],
36
+ }.reject { |_, value| value.nil? }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,34 @@
1
+ require 'rack'
2
+ require 'jwt_signed_request'
3
+
4
+ module JWTSignedRequest
5
+ module Middlewares
6
+ class Rack
7
+ UNAUTHORIZED_STATUS_CODE = 401.freeze
8
+
9
+ def initialize(app, options = {})
10
+ @app = app
11
+ @secret_key = options.fetch(:secret_key)
12
+ @algorithm = options[:algorithm]
13
+ end
14
+
15
+ def call(env)
16
+ begin
17
+ ::JWTSignedRequest.verify(
18
+ request: ::Rack::Request.new(env),
19
+ secret_key: secret_key,
20
+ algorithm: algorithm
21
+ )
22
+
23
+ app.call(env)
24
+ rescue ::JWTSignedRequest::UnauthorizedRequestError => e
25
+ [UNAUTHORIZED_STATUS_CODE, {'Content-Type' => 'application/json'} , []]
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :app, :secret_key, :algorithm
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module JWTSignedRequest
2
+ VERSION = "1.0.1".freeze
3
+ end
@@ -0,0 +1,68 @@
1
+ require 'jwt'
2
+ require 'jwt_signed_request/claims'
3
+ require 'jwt_signed_request/headers'
4
+
5
+ module JWTSignedRequest
6
+ DEFAULT_ALGORITHM = 'ES256'.freeze
7
+ EMPTY_BODY = "".freeze
8
+
9
+ UnauthorizedRequestError = Class.new(StandardError)
10
+ MissingAuthorizationHeaderError = Class.new(UnauthorizedRequestError)
11
+ JWTDecodeError = Class.new(UnauthorizedRequestError)
12
+ RequestVerificationFailedError = Class.new(UnauthorizedRequestError)
13
+
14
+ def self.sign(method:, path:, body: EMPTY_BODY, headers:, secret_key:, algorithm: DEFAULT_ALGORITHM, key_id: nil, issuer: nil, additional_headers_to_sign: Claims::EMPTY_HEADERS)
15
+ additional_jwt_headers = key_id ? {kid: key_id} : {}
16
+ JWT.encode(
17
+ Claims.generate(
18
+ method: method,
19
+ path: path,
20
+ headers: headers,
21
+ body: body,
22
+ additional_headers_to_sign: additional_headers_to_sign,
23
+ issuer: issuer
24
+ ),
25
+ secret_key,
26
+ algorithm,
27
+ additional_jwt_headers
28
+ )
29
+ end
30
+
31
+ def self.verify(request:, secret_key:, algorithm: nil)
32
+ jwt_token = Headers.fetch('Authorization', request)
33
+ algorithm ||= DEFAULT_ALGORITHM
34
+
35
+ if jwt_token.nil?
36
+ raise MissingAuthorizationHeaderError, "Missing Authorization header in the request"
37
+ end
38
+
39
+ begin
40
+ claims = JWT.decode(jwt_token, secret_key, algorithm)[0]
41
+ unless verified_request?(request: request, claims: claims)
42
+ raise RequestVerificationFailedError, "Request failed verification"
43
+ end
44
+
45
+ rescue ::JWT::DecodeError => e
46
+ raise JWTDecodeError, e.message
47
+ end
48
+ end
49
+
50
+ def self.verified_request?(request:, claims:)
51
+ claims['method'].downcase == request.request_method.downcase &&
52
+ claims['path'] == request.fullpath &&
53
+ claims['body_sha'] == Digest::SHA256.hexdigest(request.body.read || "") &&
54
+ verified_headers?(request: request, claims: claims)
55
+ end
56
+
57
+ private_class_method :verified_request?
58
+
59
+ def self.verified_headers?(request:, claims:)
60
+ parsed_headers = JSON.parse(claims['headers'])
61
+
62
+ parsed_headers.all? do |header_key, header_value|
63
+ Headers.fetch(header_key, request) == header_value
64
+ end
65
+ end
66
+
67
+ private_class_method :verified_headers?
68
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwt_signed_request
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Toan Nguyen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
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: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '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'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: ''
112
+ email:
113
+ - toan.nguyen@envato.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - README.md
119
+ - lib/jwt_signed_request.rb
120
+ - lib/jwt_signed_request/claims.rb
121
+ - lib/jwt_signed_request/headers.rb
122
+ - lib/jwt_signed_request/middlewares/faraday.rb
123
+ - lib/jwt_signed_request/middlewares/rack.rb
124
+ - lib/jwt_signed_request/version.rb
125
+ homepage: https://github.com/envato/jwt_signed_request
126
+ licenses: []
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.4.5.1
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: JWT request signing and verification for Internal APIs
148
+ test_files: []
149
+ has_rdoc: