usps-jwt_auth 1.2.2 → 1.2.4
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/Gemfile.lock +1 -1
- data/lib/usps/jwt_auth/concern.rb +39 -25
- data/lib/usps/jwt_auth/decode.rb +19 -13
- data/lib/usps/jwt_auth/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ce721a644d600d1c0d4f628bd17a1588631ce652bbf0cc5393a2cba5a21e931
|
|
4
|
+
data.tar.gz: 14d6701e1890f6c43b0c77d0c42b9b7b35a7ee32a0fd2935ed358720fa9f5882
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e85979fd43fdca1b7a625cbb8ac3c93e71e1d82eaa002805ea4fe396cc2fd05adcbdc996c058ba99089566842643b9399ff9ea00f81f92c7991f2f2726fd989d
|
|
7
|
+
data.tar.gz: a25da72d317735201c4363ec8e0ba02485a5725049c886df3ee0e1a0319f6a60e79ec3f2cbf64eb16ad1ec0597097cadf1af826aa052d1275faf96d18fbd4411
|
data/Gemfile.lock
CHANGED
|
@@ -75,18 +75,30 @@ module Usps
|
|
|
75
75
|
def set_new_jwt
|
|
76
76
|
return if params[:jwt].blank? || @set_new_jwt
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
return unless ensure_valid_jwt_has_valid_member!
|
|
78
|
+
# Nothing is stored until the token is verified, so a bad one can't disturb the current
|
|
79
|
+
# session. Fall through, and a visitor with no session is sent to login.
|
|
80
|
+
return unless ensure_valid_jwt_has_valid_member!(params[:jwt])
|
|
82
81
|
|
|
82
|
+
store_jwt(params[:jwt])
|
|
83
83
|
redirect_to_path!
|
|
84
84
|
@set_new_jwt = true
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Back to where the login began, with the query it began with. The query rides through the login
|
|
88
|
+
# as a single `query_string` parameter, and is unpacked here rather than handed on still packed.
|
|
89
|
+
# It is re-encoded rather than passed through as received, so the redirect stays well-formed
|
|
90
|
+
# whatever arrives.
|
|
87
91
|
def redirect_to_path!
|
|
88
|
-
path = params[:path]
|
|
89
|
-
|
|
92
|
+
path = local_path(params[:path])
|
|
93
|
+
pairs = URI.decode_www_form(params[:query_string].to_s).reject { |key, _value| key.empty? }
|
|
94
|
+
|
|
95
|
+
redirect_to(pairs.empty? ? path : "#{path}?#{URI.encode_www_form(pairs)}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Anything but a single leading slash could point the redirect at another host, which Rails
|
|
99
|
+
# answers with a 500.
|
|
100
|
+
def local_path(path)
|
|
101
|
+
path.is_a?(String) && path.match?(%r{\A/(?![/\\])[^\x00-\x1f\x7f]*\z}) ? path : root_path
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
def store_jwt(token)
|
|
@@ -107,36 +119,38 @@ module Usps
|
|
|
107
119
|
session[:jwt] || cookies[:jwt]
|
|
108
120
|
end
|
|
109
121
|
|
|
110
|
-
def jwt
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
issuer: Regexp.union(JwtAuth.config.issuers)
|
|
115
|
-
)
|
|
122
|
+
def jwt = decode_jwt(fetch_jwt)
|
|
123
|
+
|
|
124
|
+
def decode_jwt(token)
|
|
125
|
+
Decode.decode(token, audience: [JwtAuth.config.audience], issuer: Regexp.union(JwtAuth.config.issuers))
|
|
116
126
|
end
|
|
117
127
|
|
|
118
128
|
def redirect_to_login
|
|
119
129
|
return if @set_new_jwt
|
|
120
130
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
production = "#{url}?application=#{JwtAuth.config.audience}"
|
|
124
|
-
url = JwtAuth.config.environment.development? ? local : production
|
|
125
|
-
url = "#{url}&path=#{request.path}"
|
|
131
|
+
# Escaped so the path can't add parameters of its own to the login request.
|
|
132
|
+
url = "#{login_url}&path=#{CGI.escape(request.path)}"
|
|
126
133
|
url = "#{url}&query_string=#{CGI.escape(request.query_string)}" if request.query_string.present?
|
|
127
134
|
|
|
128
135
|
redirect_to(url, allow_other_host: true)
|
|
129
136
|
end
|
|
130
137
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
def login_url
|
|
139
|
+
url = 'https://www.usps.org/jwt/'
|
|
140
|
+
return "#{url}?local&port=#{ENV.fetch('PORT', '3000')}" if JwtAuth.config.environment.development?
|
|
141
|
+
|
|
142
|
+
"#{url}?application=#{JwtAuth.config.audience}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ensure_valid_jwt_has_valid_member!(token)
|
|
146
|
+
certificate = decode_jwt(token)['certificate']
|
|
147
|
+
# A blank certificate claim is malformed and must never resolve to a member.
|
|
148
|
+
JwtAuth.config.find_member.call(certificate) if certificate.present?
|
|
134
149
|
rescue JWT::DecodeError
|
|
135
|
-
# The token cannot be verified (e.g. an unresolvable key).
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
@current_user = nil
|
|
150
|
+
# The token cannot be verified (e.g. an unresolvable key). Report it as invalid so the
|
|
151
|
+
# caller falls through — never let this escape as an unhandled 500, which the error page
|
|
152
|
+
# would re-trigger when it re-authenticates.
|
|
153
|
+
nil
|
|
140
154
|
rescue ActiveRecord::RecordNotFound
|
|
141
155
|
reset_session
|
|
142
156
|
clear_jwt
|
data/lib/usps/jwt_auth/decode.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Usps
|
|
|
12
12
|
# Keep a slow or unreachable store from tying up the caller (e.g. a web worker).
|
|
13
13
|
OPEN_TIMEOUT = 2
|
|
14
14
|
READ_TIMEOUT = 2
|
|
15
|
+
KEY_NAME = /\A[A-Za-z0-9_-]{1,128}\z/
|
|
15
16
|
|
|
16
17
|
def self.decode(token, audience: [], issuer: nil)
|
|
17
18
|
new.decode(token, audience: audience, issuer: issuer)
|
|
@@ -29,21 +30,21 @@ module Usps
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def decode(token, audience: [], issuer: nil)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
payload, = JWT.decode(token, public_key(token), true, self.class.token_options(audience:, issuer:))
|
|
34
|
+
return payload['data'] if payload['data'].is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
raise JWT::DecodeError, 'Token has no data'
|
|
37
|
+
rescue ArgumentError, TypeError, NoMethodError, OpenSSL::PKey::PKeyError => e
|
|
38
|
+
# Whatever a token holds, callers only need to handle JWT::DecodeError.
|
|
39
|
+
raise JWT::DecodeError, "Unreadable token: #{e.class}"
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
private
|
|
42
43
|
|
|
43
44
|
# Resolve the public key the token names by its fingerprint. Fast path is the local
|
|
44
45
|
# cache; on a miss we fetch it once from the shared store and cache it permanently
|
|
45
|
-
# (a fingerprint maps to one key forever).
|
|
46
|
-
#
|
|
46
|
+
# (a fingerprint maps to one key forever). A token can name any key in the store, so
|
|
47
|
+
# write access to the store is as sensitive as a signing key.
|
|
47
48
|
def public_key(token)
|
|
48
49
|
fingerprint = fingerprint(token)
|
|
49
50
|
OpenSSL::PKey::RSA.new(File.read(cache_path(fingerprint)))
|
|
@@ -70,9 +71,9 @@ module Usps
|
|
|
70
71
|
raise JWT::VerificationError, "Could not fetch public key for token fingerprint: #{e.message}"
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
74
|
+
# Use nothing the store returns until the key reproduces the fingerprint the token names.
|
|
75
|
+
# This stops the store or CDN swapping in a different key for a known fingerprint. Only
|
|
76
|
+
# verified keys are written to the cache.
|
|
76
77
|
def cache_verified_key(fingerprint, pem)
|
|
77
78
|
key = OpenSSL::PKey::RSA.new(pem)
|
|
78
79
|
unless Fingerprint.for(key.public_key) == fingerprint
|
|
@@ -105,8 +106,13 @@ module Usps
|
|
|
105
106
|
response.body
|
|
106
107
|
end
|
|
107
108
|
|
|
109
|
+
# Read before the signature is checked, so this claim can only ever name a key file. Its
|
|
110
|
+
# alphabet leaves no way to form a path.
|
|
108
111
|
def fingerprint(token)
|
|
109
|
-
JWT.decode(token, nil, false)[0]['key']
|
|
112
|
+
name = JWT.decode(token, nil, false)[0]['key']
|
|
113
|
+
return name if name.is_a?(String) && name.match?(KEY_NAME)
|
|
114
|
+
|
|
115
|
+
raise JWT::VerificationError, 'Token names an invalid key'
|
|
110
116
|
end
|
|
111
117
|
end
|
|
112
118
|
end
|