auth0_rs256_jwt_verifier 0.0.1
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 +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +9 -0
- data/Gemfile +8 -0
- data/README.md +6 -0
- data/Rakefile +10 -0
- data/auth0_rs256_jwt_verifier.gemspec +25 -0
- data/lib/auth0_access_tokens_rs256_verifier.rb +44 -0
- data/lib/auth0_rs256_jwt_verifier.rb +44 -0
- data/lib/auth0_rs256_jwt_verifier/certs_set.rb +31 -0
- data/lib/auth0_rs256_jwt_verifier/exp_verifier.rb +8 -0
- data/lib/auth0_rs256_jwt_verifier/jwk.rb +138 -0
- data/lib/auth0_rs256_jwt_verifier/jwk_set_downloader.rb +41 -0
- data/lib/auth0_rs256_jwt_verifier/jwt_decoder.rb +38 -0
- data/lib/auth0_rs256_jwt_verifier/jwt_decoder_wrapper.rb +102 -0
- data/lib/auth0_rs256_jwt_verifier/results.rb +56 -0
- data/lib/auth0_rs256_jwt_verifier/user_id.rb +25 -0
- data/lib/auth0_rs256_jwt_verifier/valid_jwk_set.rb +27 -0
- data/test/.rubocop.yml +11 -0
- data/test/auth0_rs256_jwt_verifier/jwt_decoder_test.rb +21 -0
- data/test/auth0_rs256_jwt_verifier/jwt_decoder_wrapper_test.rb +237 -0
- data/test/auth0_rs256_jwt_verifier_test.rb +119 -0
- data/test/fixtures/sample_jwks.json +16 -0
- data/test/fixtures/sample_jwks_2.json +1 -0
- data/test/test_helper.rb +3 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 62a95cfb84c90e4b8059e318f2c7bfcee71e8b27
|
4
|
+
data.tar.gz: 9e6cde7f7da12eeb4ddde673900eb9eb18b2a5d3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e5d78f48de5792166fff6903c7f3740be6bf1f5bb279971deba1363ce5e5151866d22b09f74508fb44c50248193372ab345410b2af921a7f9ba493b3a8eefd4e
|
7
|
+
data.tar.gz: 93a0adcddc9c89f60e846fe4331a1308d5c4c04d35098206d5cf105abea32ab06a2b36622cdda900e6d95cec243430bdfe8560f73e982153f682dcf591c8c397
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# Auth0 JWT (RS256) verification library
|
2
|
+
[Auth0](https://auth0.com) is web service handling users identities which can be easily plugged
|
3
|
+
into your application. It provides [SDKs](https://auth0.com/docs) for many languages which enable you to sign up/in users
|
4
|
+
and returns access token ([JWT](https://jwt.io)) in exchange. Access token can be used then to access your's Web Service.
|
5
|
+
This gem helps you to [verify](https://auth0.com/docs/api-auth/tutorials/verify-access-token#verify-the-signature)
|
6
|
+
such access token which has been signed using the RS256 algorithm.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = "auth0_rs256_jwt_verifier"
|
4
|
+
s.version = "0.0.1"
|
5
|
+
s.date = "2017-06-12"
|
6
|
+
s.summary = "Auth0 JWT (RS256) verification library"
|
7
|
+
s.description = <<-DESCRIPTION.gsub(/\s+/, " ").strip
|
8
|
+
Auth0 (https://auth0.com) is web service handling users identities which can be easily plugged
|
9
|
+
into your application. It provides SDKs for many languages which enable you to sign up/in users
|
10
|
+
and returns access token (JWT) in exchange. Access token can be used then to access your's Web Service.
|
11
|
+
This gem helps you to verify
|
12
|
+
(https://auth0.com/docs/api-auth/tutorials/verify-access-token#verify-the-signature)
|
13
|
+
such access token which has been signed using the RS256 algorithm.
|
14
|
+
DESCRIPTION
|
15
|
+
s.authors = ["Krzysztof Zielonka"]
|
16
|
+
s.email = "krzysztof.zielonka@droidsonroids.pl"
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test}/*`.split("\n")
|
19
|
+
s.homepage = "https://rubygems.org/gems/auth0_rs256_jwt_verifier"
|
20
|
+
s.license = "MIT"
|
21
|
+
s.add_runtime_dependency "http", "~> 2"
|
22
|
+
s.add_runtime_dependency "json-jwt", "~> 1.7"
|
23
|
+
s.add_development_dependency "rake", "~> 12"
|
24
|
+
s.add_development_dependency "minitest", "~> 5"
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "http"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
require "auth0_jwt_rs256_verifier/user_id"
|
6
|
+
require "auth0_jwt_rs256_verifier/results"
|
7
|
+
require "auth0_jwt_rs256_verifier/jwt_decoder"
|
8
|
+
require "auth0_jwt_rs256_verifier/jwt_decoder_wrapper"
|
9
|
+
require "auth0_jwt_rs256_verifier/jwk_set_downloader"
|
10
|
+
require "auth0_jwt_rs256_verifier/valid_jwk_set"
|
11
|
+
require "auth0_jwt_rs256_verifier/certs_set"
|
12
|
+
require "auth0_jwt_rs256_verifier/exp_verifier"
|
13
|
+
require "auth0_jwt_rs256_verifier/jwk"
|
14
|
+
|
15
|
+
class Auth0RS256JWTVerifier
|
16
|
+
def initialize(issuer:, audience:, jwks_url:, http: HTTP, exp_verifier: ExpVerifier.new)
|
17
|
+
@audience = audience
|
18
|
+
@issuer = issuer
|
19
|
+
@jwks_url = jwks_url
|
20
|
+
@jwks_downloader = JWKSetDownloader.new(http)
|
21
|
+
@exp_verifier = exp_verifier
|
22
|
+
@certificates = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify(access_token)
|
26
|
+
payload = JWTDecoderWrapper.new(
|
27
|
+
@audience,
|
28
|
+
@issuer,
|
29
|
+
certificates,
|
30
|
+
exp_verifier: @exp_verifier,
|
31
|
+
jwt_decoder: JWTDecoder.new,
|
32
|
+
).decode(access_token)
|
33
|
+
Results::ValidAccessToken.new(UserId.new(payload.sub))
|
34
|
+
rescue JWTDecoderWrapper::Error
|
35
|
+
Results::INVALID_ACCESS_TOKEN
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def certificates
|
41
|
+
return @certificates if @certificates
|
42
|
+
@certificates = CertsSet.new(ValidJWKSet.new(@jwks_downloader.download(@jwks_url)))
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "http"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
require "auth0_rs256_jwt_verifier/user_id"
|
6
|
+
require "auth0_rs256_jwt_verifier/results"
|
7
|
+
require "auth0_rs256_jwt_verifier/jwt_decoder"
|
8
|
+
require "auth0_rs256_jwt_verifier/jwt_decoder_wrapper"
|
9
|
+
require "auth0_rs256_jwt_verifier/jwk_set_downloader"
|
10
|
+
require "auth0_rs256_jwt_verifier/valid_jwk_set"
|
11
|
+
require "auth0_rs256_jwt_verifier/certs_set"
|
12
|
+
require "auth0_rs256_jwt_verifier/exp_verifier"
|
13
|
+
require "auth0_rs256_jwt_verifier/jwk"
|
14
|
+
|
15
|
+
class Auth0RS256JWTVerifier
|
16
|
+
def initialize(issuer:, audience:, jwks_url:, http: HTTP, exp_verifier: ExpVerifier.new)
|
17
|
+
@audience = audience
|
18
|
+
@issuer = issuer
|
19
|
+
@jwks_url = jwks_url
|
20
|
+
@jwks_downloader = JWKSetDownloader.new(http)
|
21
|
+
@exp_verifier = exp_verifier
|
22
|
+
@certificates = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify(access_token)
|
26
|
+
payload = JWTDecoderWrapper.new(
|
27
|
+
@audience,
|
28
|
+
@issuer,
|
29
|
+
certificates,
|
30
|
+
exp_verifier: @exp_verifier,
|
31
|
+
jwt_decoder: JWTDecoder.new,
|
32
|
+
).decode(access_token)
|
33
|
+
Results::ValidAccessToken.new(UserId.new(payload.sub))
|
34
|
+
rescue JWTDecoderWrapper::Error
|
35
|
+
Results::INVALID_ACCESS_TOKEN
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def certificates
|
41
|
+
return @certificates if @certificates
|
42
|
+
@certificates = CertsSet.new(ValidJWKSet.new(@jwks_downloader.download(@jwks_url)))
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "base64"
|
3
|
+
class Auth0RS256JWTVerifier
|
4
|
+
class CertsSet
|
5
|
+
NotFoundError = Class.new(RuntimeError)
|
6
|
+
|
7
|
+
def initialize(jwk_set)
|
8
|
+
@jwk_set = jwk_set
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(id)
|
12
|
+
cert = certs.find { |c| c.id == id }
|
13
|
+
raise NotFoundError, "cert #{id} doesn't exist" if cert.nil?
|
14
|
+
cert.cert
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
CertWithId = Struct.new(:id, :cert)
|
20
|
+
private_constant :CertWithId
|
21
|
+
|
22
|
+
def certs
|
23
|
+
@certs ||= @jwk_set.map { |jwk| CertWithId.new(jwk.kid, build_cert(jwk)) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_cert(jwk)
|
27
|
+
encoded = Base64.decode64(String(jwk.x5c.first))
|
28
|
+
OpenSSL::X509::Certificate.new(encoded)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Auth0RS256JWTVerifier
|
3
|
+
class JWK
|
4
|
+
ParseError = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
def inspect
|
7
|
+
"JWK(\n" \
|
8
|
+
"\talg: #{@alg},\n" \
|
9
|
+
"\tkty: #{@kty},\n" \
|
10
|
+
"\tuse: #{@use},\n" \
|
11
|
+
"\tx5c: #{@x5c.inspect.split("\n").map { |l| "\t#{l}" }.join("\n")},\n" \
|
12
|
+
"\tn: #{@n},\n" \
|
13
|
+
"\te: #{@e},\n" \
|
14
|
+
"\tkid: #{@kid},\n" \
|
15
|
+
"\tx5t: #{@x5t}\n" \
|
16
|
+
")"
|
17
|
+
end
|
18
|
+
|
19
|
+
class JWKMember
|
20
|
+
def present?
|
21
|
+
raise NotImplementedMethod
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class OptionalStringJWKMember
|
26
|
+
include Comparable
|
27
|
+
|
28
|
+
def initialize(value)
|
29
|
+
if value.nil?
|
30
|
+
@value = nil
|
31
|
+
elsif value.is_a?(String)
|
32
|
+
@value = value
|
33
|
+
else
|
34
|
+
raise ParseError, "require field #{self.class.name} to be String but is '#{value}'"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def <=>(other)
|
39
|
+
@value <=> String(other)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
@value
|
44
|
+
end
|
45
|
+
|
46
|
+
def present?
|
47
|
+
!@value.nil?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
private_constant :OptionalStringJWKMember
|
51
|
+
|
52
|
+
class RequiredStringJWKMember
|
53
|
+
include Comparable
|
54
|
+
|
55
|
+
def initialize(value)
|
56
|
+
if value.is_a?(String)
|
57
|
+
@value = value
|
58
|
+
elsif value.nil?
|
59
|
+
raise PraseError, "field #{self.class.name} is required"
|
60
|
+
else
|
61
|
+
raise ParseError, "require field #{self.class.name} to be String but is '#{value}'"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def <=>(other)
|
66
|
+
@value <=> String(other)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
@value
|
71
|
+
end
|
72
|
+
|
73
|
+
def present?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
private_constant :RequiredStringJWKMember
|
78
|
+
|
79
|
+
Kty = Class.new(RequiredStringJWKMember)
|
80
|
+
Use = Class.new(OptionalStringJWKMember)
|
81
|
+
Alg = Class.new(OptionalStringJWKMember)
|
82
|
+
N = Class.new(OptionalStringJWKMember)
|
83
|
+
E = Class.new(OptionalStringJWKMember)
|
84
|
+
Kid = Class.new(OptionalStringJWKMember)
|
85
|
+
X5T = Class.new(OptionalStringJWKMember)
|
86
|
+
|
87
|
+
class X5C
|
88
|
+
include Enumerable
|
89
|
+
|
90
|
+
class Certificate
|
91
|
+
def initialize(certificate)
|
92
|
+
raise ParseError unless certificate.is_a?(String)
|
93
|
+
@certificate = certificate
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_s
|
97
|
+
@certificate
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_str
|
101
|
+
to_s
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def initialize(certificates)
|
106
|
+
if certificates.nil?
|
107
|
+
@certificates = nil
|
108
|
+
else
|
109
|
+
raise ParseError unless certificates.is_a?(Array)
|
110
|
+
@certificates = certificates.map { |certificate| Certificate.new(certificate) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def inspect
|
115
|
+
"X5C(\n#{@certificates.map { |c| "\t#{c}" }.join(",\n")}\n\t)"
|
116
|
+
end
|
117
|
+
|
118
|
+
def present?
|
119
|
+
!@certificates.nil?
|
120
|
+
end
|
121
|
+
|
122
|
+
def each
|
123
|
+
return unless present?
|
124
|
+
@certificates.each { |cert| yield cert }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def initialize(hash)
|
129
|
+
raise ParseError unless hash.is_a?(Hash)
|
130
|
+
%i(Alg Kty Use X5C N E Kid X5T).each do |field_name|
|
131
|
+
field = self.class.const_get(field_name).new(hash[String(field_name).downcase])
|
132
|
+
instance_variable_set("@#{String(field_name).downcase}", field)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
attr_reader :alg, :kty, :use, :x5c, :n, :e, :kid, :x5t
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Auth0RS256JWTVerifier
|
3
|
+
class JWKSetDownloader
|
4
|
+
InvalidJWKSetError = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
class JWKSet
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
ParseError = Class.new(RuntimeError)
|
10
|
+
|
11
|
+
def initialize(hash)
|
12
|
+
raise ParseError if hash["keys"].is_a?(Hash)
|
13
|
+
@keys = hash["keys"].map { |key| JWK.new(key) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
@keys.each { |key| yield key }
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"JWKSet(#{@keys.collect(&:inspect).join(", ")})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
private_constant :JWKSet
|
25
|
+
|
26
|
+
def initialize(http)
|
27
|
+
@http = http
|
28
|
+
end
|
29
|
+
|
30
|
+
def download(url)
|
31
|
+
body = @http.get(url)
|
32
|
+
json = JSON.parse(body)
|
33
|
+
begin
|
34
|
+
JWKSet.new(json)
|
35
|
+
rescue JWKSet::ParseError
|
36
|
+
raise InvalidJWKSetError
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
private_constant :JWKSetDownloader
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "json/jwt"
|
3
|
+
|
4
|
+
class Auth0RS256JWTVerifier
|
5
|
+
class JWTDecoder
|
6
|
+
def decode(jwt_str)
|
7
|
+
jwt = JSON::JWT.decode(jwt_str, :skip_verification)
|
8
|
+
DecodedJWT.new(jwt)
|
9
|
+
end
|
10
|
+
|
11
|
+
def signed_with?(jwt_str, public_key)
|
12
|
+
JSON::JWT.decode(jwt_str, public_key)
|
13
|
+
true
|
14
|
+
rescue JSON::JWS::VerificationFailed
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
class DecodedJWT
|
19
|
+
def initialize(jwt)
|
20
|
+
@jwt = jwt
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](k)
|
24
|
+
case k
|
25
|
+
when :alg then @jwt.alg
|
26
|
+
when :kid then @jwt.header[:kid]
|
27
|
+
else @jwt[k]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def key?(k)
|
32
|
+
![k].nil?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
private_constant :DecodedJWT
|
36
|
+
end
|
37
|
+
private_constant :JWTDecoder
|
38
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Auth0RS256JWTVerifier
|
3
|
+
class JWTDecoderWrapper
|
4
|
+
Payload = Struct.new(:sub)
|
5
|
+
private_constant :Payload
|
6
|
+
|
7
|
+
Error = Class.new(RuntimeError)
|
8
|
+
InvalidJWTError = Class.new(Error)
|
9
|
+
InvalidAlgError = Class.new(Error)
|
10
|
+
InvalidAudienceError = Class.new(Error)
|
11
|
+
InvalidIssuerError = Class.new(Error)
|
12
|
+
MissingSubError = Class.new(Error)
|
13
|
+
InvalidSubError = Class.new(Error)
|
14
|
+
VerificationError = Class.new(Error)
|
15
|
+
MissingExpError = Class.new(Error)
|
16
|
+
InvalidExpError = Class.new(Error)
|
17
|
+
JWTExpiredError = Class.new(Error)
|
18
|
+
CertNotFoundError = Class.new(Error)
|
19
|
+
|
20
|
+
def initialize(audience, issuer, certificates, exp_verifier:, jwt_decoder:)
|
21
|
+
@audience = audience
|
22
|
+
@issuer = issuer
|
23
|
+
@certificates = certificates
|
24
|
+
@exp_verifier = exp_verifier
|
25
|
+
@jwt_decoder = jwt_decoder
|
26
|
+
end
|
27
|
+
|
28
|
+
def decode(jwt_str)
|
29
|
+
jwt_str = String(jwt_str)
|
30
|
+
|
31
|
+
decoded_jwt = raw_decode(jwt_str)
|
32
|
+
|
33
|
+
verify_alg(decoded_jwt)
|
34
|
+
|
35
|
+
public_key = find_public_key_for(decoded_jwt)
|
36
|
+
verify_is_signed(jwt_str, public_key)
|
37
|
+
|
38
|
+
# verify JWT
|
39
|
+
verify_expiration_time(decoded_jwt)
|
40
|
+
verify_audience(decoded_jwt)
|
41
|
+
verify_issuer(decoded_jwt)
|
42
|
+
verify_sub(decoded_jwt)
|
43
|
+
|
44
|
+
Payload.new(decoded_jwt[:sub])
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def raw_decode(jwt_str)
|
50
|
+
@jwt_decoder.decode(jwt_str)
|
51
|
+
rescue StandardError => e
|
52
|
+
raise InvalidJWTError, e.message
|
53
|
+
end
|
54
|
+
|
55
|
+
def verify_alg(decoded_jwt)
|
56
|
+
alg = decoded_jwt[:alg]
|
57
|
+
raise InvalidAlgError, "alg should be RS256 but is #{alg}" unless alg == "RS256"
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_public_key_for(decoded_jwt)
|
61
|
+
kid = decoded_jwt[:kid]
|
62
|
+
@certificates.find(kid).public_key
|
63
|
+
rescue CertsSet::NotFoundError => e
|
64
|
+
raise CertNotFoundError, e.message
|
65
|
+
end
|
66
|
+
|
67
|
+
def verify_is_signed(jwt_str, public_key)
|
68
|
+
raise VerificationError unless @jwt_decoder.signed_with?(jwt_str, public_key)
|
69
|
+
rescue StandardError => e
|
70
|
+
raise VerificationError, e.message
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify_expiration_time(decoded_jwt)
|
74
|
+
verify_exp_exist(decoded_jwt)
|
75
|
+
verify_exp_is_int(decoded_jwt)
|
76
|
+
raise JWTExpiredError, "jwt expired" if @exp_verifier.expired?(decoded_jwt[:exp])
|
77
|
+
end
|
78
|
+
|
79
|
+
def verify_exp_exist(decoded_jwt)
|
80
|
+
raise MissingExpError, "missing 'exp' jwt key" unless decoded_jwt.key?(:exp)
|
81
|
+
end
|
82
|
+
|
83
|
+
def verify_exp_is_int(decoded_jwt)
|
84
|
+
return if decoded_jwt[:exp].is_a?(Integer)
|
85
|
+
raise InvalidExpError, "jwt 'exp' field must be an integer"
|
86
|
+
end
|
87
|
+
|
88
|
+
def verify_audience(decoded_jwt)
|
89
|
+
raise InvalidAudienceError unless Array(decoded_jwt[:aud]).include?(@audience)
|
90
|
+
end
|
91
|
+
|
92
|
+
def verify_issuer(decoded_jwt)
|
93
|
+
raise InvalidIssuerError unless decoded_jwt[:iss] == @issuer
|
94
|
+
end
|
95
|
+
|
96
|
+
def verify_sub(decoded_jwt)
|
97
|
+
raise MissingSubError unless decoded_jwt.key?(:sub)
|
98
|
+
raise InvalidSubError unless decoded_jwt[:sub].is_a?(String)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
private_constant :JWTDecoderWrapper
|
102
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Auth0RS256JWTVerifier
|
3
|
+
module Results
|
4
|
+
class Base
|
5
|
+
def valid?
|
6
|
+
raise NotImplementedMethod
|
7
|
+
end
|
8
|
+
|
9
|
+
def invalid?
|
10
|
+
!valid?
|
11
|
+
end
|
12
|
+
|
13
|
+
def on(_)
|
14
|
+
raise NotImplementedMethod
|
15
|
+
end
|
16
|
+
end
|
17
|
+
private_constant :Base
|
18
|
+
|
19
|
+
class ValidAccessToken < Base
|
20
|
+
def initialize(user_id)
|
21
|
+
@user_id = user_id
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :user_id
|
25
|
+
|
26
|
+
def valid?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def on(type)
|
31
|
+
yield @user_id if type == :valid
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"Auth0RS256JWTVerifier::Results::ValidAccessToken(user_id: #{@user_id})"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
INVALID_ACCESS_TOKEN = Class.new(Base) do
|
41
|
+
def valid?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
def on(type)
|
46
|
+
yield if type == :invalid
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def inspect
|
51
|
+
"Auth0RS256JWTVerifier::Results::INVALID_ACCESS_TOKEN"
|
52
|
+
end
|
53
|
+
end.new.freeze
|
54
|
+
end
|
55
|
+
private_constant :Results
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Auth0RS256JWTVerifier
|
3
|
+
class UserId
|
4
|
+
def initialize(id)
|
5
|
+
@id = String(id).dup.freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
@id
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_str
|
13
|
+
to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
String(self) == String(other)
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"Auth0RS256JWTVerifier::UserId(#{@id})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
private_constant :UserId
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Auth0RS256JWTVerifier
|
3
|
+
class ValidJWKSet
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(jwk_set)
|
7
|
+
@jwk_set = jwk_set
|
8
|
+
end
|
9
|
+
|
10
|
+
def each
|
11
|
+
filtered.each { |jwk| yield jwk }
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def filtered
|
17
|
+
@filtered ||= @jwk_set.select { |jwk| valid_jwk?(jwk) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_jwk?(jwk)
|
21
|
+
jwk.use == "sig" &&
|
22
|
+
jwk.kty == "RSA" &&
|
23
|
+
jwk.kid.present? &&
|
24
|
+
jwk.x5c.any?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/test/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class Auth0RS256JWTVerifier
|
5
|
+
describe JWTDecoder do
|
6
|
+
before :each do
|
7
|
+
@decoder = JWTDecoder.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should decodde simple jwt" do
|
11
|
+
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImlkMTIzNCJ9." \
|
12
|
+
"eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiYXVkaWVuY2UiLCJpc3MiO" \
|
13
|
+
"iJpc3N1ZXIifQ.cp374RbcG-q8rTLxSoWtLK7dtn5cBa3_g4riKL9OSt0"
|
14
|
+
result = @decoder.decode(jwt)
|
15
|
+
assert_equal "HS256", result[:alg]
|
16
|
+
assert_equal "id1234", result[:kid]
|
17
|
+
assert_equal "audience", result[:aud]
|
18
|
+
assert_equal "issuer", result[:iss]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class Auth0RS256JWTVerifier
|
5
|
+
describe JWTDecoderWrapper do
|
6
|
+
class JWTDecoderWrapperFake
|
7
|
+
def decode(_jwt)
|
8
|
+
{}
|
9
|
+
end
|
10
|
+
|
11
|
+
def signed_with?(_jwt, _public_key)
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class CertsSetFake
|
17
|
+
def find(_id)
|
18
|
+
CertFake.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class CertFake
|
23
|
+
def public_key
|
24
|
+
"ascd"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class ExpVerifierFake
|
29
|
+
def expired?(_)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
before :all do
|
35
|
+
@decoder = JWTDecoderWrapperFake.new
|
36
|
+
@certs_set = CertsSetFake.new
|
37
|
+
@exp_verifier = ExpVerifierFake.new
|
38
|
+
@audience = "valid audience"
|
39
|
+
@issuer = "valid issuer"
|
40
|
+
|
41
|
+
@jwt_decoder = factory_subject
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should execute JWT#decode & JWT#verify with valid args" do
|
45
|
+
jwt_str = "JWT to decode"
|
46
|
+
public_key = "valid public key"
|
47
|
+
cert = Object.new
|
48
|
+
cert.define_singleton_method(:public_key) { public_key }
|
49
|
+
|
50
|
+
@decoder = Minitest::Mock.new
|
51
|
+
@decoder.expect(:decode, valid_decoded_jwt, [jwt_str])
|
52
|
+
@decoder.expect(:signed_with?, valid_decoded_jwt, [jwt_str, public_key])
|
53
|
+
|
54
|
+
@jwt_decoder = factory_subject(jwt_decoder: @decoder)
|
55
|
+
|
56
|
+
@certs_set.stub(:find, cert) do
|
57
|
+
@jwt_decoder.decode(jwt_str)
|
58
|
+
end
|
59
|
+
|
60
|
+
@decoder.verify
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should raise InvalidJWTError if adapter#decode raises standard error" do
|
64
|
+
@decoder.stub(:decode, ->(*_) { raise StandardError }) do
|
65
|
+
assert_raises(JWTDecoderWrapper::InvalidJWTError) do
|
66
|
+
@jwt_decoder.decode("jwt")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should raise InvalidAlgError" do
|
72
|
+
@decoder.stub(
|
73
|
+
:decode,
|
74
|
+
valid_decoded_jwt(alg: "HS256"),
|
75
|
+
) do
|
76
|
+
assert_raises(JWTDecoderWrapper::InvalidAlgError) do
|
77
|
+
@jwt_decoder.decode("jwt")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should raise CertNotFoundError" do
|
83
|
+
@decoder.stub(
|
84
|
+
:decode,
|
85
|
+
valid_decoded_jwt(kid: "unexisting key"),
|
86
|
+
) do
|
87
|
+
@certs_set.stub(
|
88
|
+
:find,
|
89
|
+
->(*_) { raise CertsSet::NotFoundError },
|
90
|
+
) do
|
91
|
+
assert_raises(JWTDecoderWrapper::CertNotFoundError) do
|
92
|
+
@jwt_decoder.decode("jwt")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should raise VerificationError" do
|
99
|
+
@decoder.stub(:decode, valid_decoded_jwt) do
|
100
|
+
@decoder.stub(:signed_with?, false) do
|
101
|
+
assert_raises(JWTDecoderWrapper::VerificationError) do
|
102
|
+
@jwt_decoder.decode("jwt")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should raise VerificationError if adapter#signed_with raises StandardError" do
|
109
|
+
@decoder.stub(:decode, valid_decoded_jwt) do
|
110
|
+
@decoder.stub(:signed_with?, ->(*_) { raise StandardError }) do
|
111
|
+
assert_raises(JWTDecoderWrapper::VerificationError) do
|
112
|
+
@jwt_decoder.decode("jwt")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should raise missing exp error" do
|
119
|
+
@decoder.stub(
|
120
|
+
:decode,
|
121
|
+
valid_decoded_jwt.reject { |k, _| k == :exp },
|
122
|
+
) do
|
123
|
+
assert_raises(JWTDecoderWrapper::MissingExpError) do
|
124
|
+
@jwt_decoder.decode("jwt")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should raise invalid exp error" do
|
130
|
+
@decoder.stub(
|
131
|
+
:decode,
|
132
|
+
valid_decoded_jwt(exp: "1234"),
|
133
|
+
) do
|
134
|
+
assert_raises(JWTDecoderWrapper::InvalidExpError) do
|
135
|
+
@jwt_decoder.decode("jwt")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should raise jwt expired error" do
|
141
|
+
@decoder.stub(
|
142
|
+
:decode,
|
143
|
+
valid_decoded_jwt(exp: 1234),
|
144
|
+
) do
|
145
|
+
@exp_verifier.stub(:expired?, true) do
|
146
|
+
assert_raises(JWTDecoderWrapper::JWTExpiredError) do
|
147
|
+
@jwt_decoder.decode("jwt")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should raise invalid audience error" do
|
154
|
+
@decoder.stub(
|
155
|
+
:decode,
|
156
|
+
valid_decoded_jwt(aud: "invalid audience"),
|
157
|
+
) do
|
158
|
+
assert_raises(JWTDecoderWrapper::InvalidAudienceError) do
|
159
|
+
@jwt_decoder.decode("jwt")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should raise invalid issuer error" do
|
165
|
+
@decoder.stub(
|
166
|
+
:decode,
|
167
|
+
valid_decoded_jwt(iss: "invalid issuer"),
|
168
|
+
) do
|
169
|
+
assert_raises(JWTDecoderWrapper::InvalidIssuerError) do
|
170
|
+
@jwt_decoder.decode("jwt")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should raise missing sub error" do
|
176
|
+
@decoder.stub(
|
177
|
+
:decode,
|
178
|
+
valid_decoded_jwt.reject { |k, _| k == :sub },
|
179
|
+
) do
|
180
|
+
assert_raises(JWTDecoderWrapper::MissingSubError) do
|
181
|
+
@jwt_decoder.decode("jwt")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should raise invalid sub error" do
|
187
|
+
@decoder.stub(
|
188
|
+
:decode,
|
189
|
+
valid_decoded_jwt(sub: { id: 1 }),
|
190
|
+
) do
|
191
|
+
assert_raises(JWTDecoderWrapper::InvalidSubError) do
|
192
|
+
@jwt_decoder.decode("jwt")
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should raise nothing and return payload with sub" do
|
198
|
+
sub = "sub1234"
|
199
|
+
@decoder.stub(
|
200
|
+
:decode,
|
201
|
+
valid_decoded_jwt(sub: sub),
|
202
|
+
) do
|
203
|
+
begin
|
204
|
+
payload = @jwt_decoder.decode("jwt")
|
205
|
+
assert_respond_to payload, :sub
|
206
|
+
assert_equal sub, payload.sub
|
207
|
+
rescue StandardError => e
|
208
|
+
refute "expected to not return any error buty got #{e}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
def factory_subject(dependencies = {})
|
216
|
+
default_dependencies = {
|
217
|
+
exp_verifier: @exp_verifier,
|
218
|
+
jwt_decoder: @decoder,
|
219
|
+
}
|
220
|
+
JWTDecoderWrapper.new(
|
221
|
+
@audience, @issuer, @certs_set,
|
222
|
+
default_dependencies.merge(dependencies)
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
def valid_decoded_jwt(overwrites = {})
|
227
|
+
{
|
228
|
+
alg: "RS256",
|
229
|
+
kid: "id1",
|
230
|
+
exp: 1234,
|
231
|
+
aud: @audience,
|
232
|
+
iss: @issuer,
|
233
|
+
sub: "1234",
|
234
|
+
}.merge(overwrites)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
describe Auth0RS256JWTVerifier do
|
5
|
+
it "verifies successfully access token" do
|
6
|
+
auth0 = Auth0RS256JWTVerifier.new(
|
7
|
+
issuer: "https://multi-jobbers.eu.auth0.com/",
|
8
|
+
audience: "https://multijobbers.herokuapp.com/",
|
9
|
+
jwks_url: "https://multi-jobbers.eu.auth0.com/.well-known/jwks.json",
|
10
|
+
http: http_stub,
|
11
|
+
exp_verifier: exp_verifier_stub,
|
12
|
+
)
|
13
|
+
|
14
|
+
verification_result = auth0.verify(
|
15
|
+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5rTkJPRFEzUWpNeFF6WkVOa1" \
|
16
|
+
"kzUVVNMk9VTTFSVGMxUTBZMk4wUXdSVGRHTWpkRk9UQkROdyJ9.eyJpc3MiOiJodHRwczo" \
|
17
|
+
"vL211bHRpLWpvYmJlcnMuZXUuYXV0aDAuY29tLyIsInN1YiI6IjZwM2tFc0pkOUhteGFxV" \
|
18
|
+
"VIwdXN3c1VFRUdoZTNuQ05IQGNsaWVudHMiLCJhdWQiOiJodHRwczovL211bHRpam9iYmV" \
|
19
|
+
"ycy5oZXJva3VhcHAuY29tLyIsImV4cCI6MTQ5NzE4MTMzNSwiaWF0IjoxNDk3MDk0OTM1L" \
|
20
|
+
"CJzY29wZSI6IiJ9.LPtyDb26UxS5NKHEU2VJdmj7pDI4-tfgue2Ttk62H0a9XehsCArwwy" \
|
21
|
+
"QtI2ZAXxY8gQGS4dhXpcDqevmpfAy9zcjMEvvjWqmcGpepL8bn4MUJ_lAmL3A3FJXduf8T" \
|
22
|
+
"pHRXUHiMcdGT0vcFrv5kkMHDzTiwvOUxPcRT5nufX16Vqg3MTQS5pDb2NPcLCqI4PrJhse" \
|
23
|
+
"uJnDthxYelUvf6AIyVesuK5e3g8FLiXjZoPmwr3u6xeljF2KECetBPskKI8MgWrhIDD9Zv" \
|
24
|
+
"-O_fV1UZ41M-7zURcsQNYV--knHuX0i6nF46JlnbdqoA35d8LJvtnzbiO7hj8mP_GMa8FS" \
|
25
|
+
"cKqq5D8w",
|
26
|
+
)
|
27
|
+
|
28
|
+
expected_user_id = "6p3kEsJd9HmxaqUR0uswsUEEGhe3nCNH@clients"
|
29
|
+
|
30
|
+
assert verification_result.valid?
|
31
|
+
refute verification_result.invalid?
|
32
|
+
assert_equal expected_user_id, verification_result.user_id
|
33
|
+
end
|
34
|
+
|
35
|
+
it "fails when jwt public key verification is not successful" do
|
36
|
+
auth0 = Auth0RS256JWTVerifier.new(
|
37
|
+
issuer: "https://example.eu.auth0.com/",
|
38
|
+
audience: "https://multijobbers.herokuapp.com/",
|
39
|
+
jwks_url: "https://multi-jobbers.eu.auth0.com/.well-known/jwks.json",
|
40
|
+
http: http_stub,
|
41
|
+
exp_verifier: exp_verifier_stub,
|
42
|
+
)
|
43
|
+
|
44
|
+
verification_result = auth0.verify(
|
45
|
+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5rTkJPRFEzUWpNeFF6WkVOa1" \
|
46
|
+
"kzUVVNMk9VTTFSVGMxUTBZMk4wUXdSVGRHTWpkRk9UQkROdyJ9.eyJpc3MiOiJodHRwczo" \
|
47
|
+
"vL211bHRpLWpvYmJlcnMuZXUuYXV0aDAuY29tLyIsInN1YiI6IjZwM2tFc0pkOUhteGFxV" \
|
48
|
+
"VIwdXN3c1VFRUdoZTNuQ05IQGNsaWVudHMiLCJhdWQiOiJodHRwczovL211bHRpam9iYmV" \
|
49
|
+
"ycy5oZXJva3VhcHAuY29tLyIsImV4cCI6MTQ5NzQ3NzYyOCwiaWF0IjoxNDk3MzkxMjI4L" \
|
50
|
+
"CJzY29wZSI6IiJ9.aGhyzkMM7sE4FNSijzRJlIvJQwx4tBq8uJbL0Taq9I41YOuSWPF4eC" \
|
51
|
+
"8886EU3gLkOiEpYlSkX9SHINljHR2ajcNBXoCThbREyReY_ZDjNfXZRREYWT6x8wT5WtmM" \
|
52
|
+
"xdxpOOVIXKrxkhfy57vGJs2clpo2MTFEVNPYhslMv-p_WLY",
|
53
|
+
)
|
54
|
+
|
55
|
+
refute verification_result.valid?
|
56
|
+
assert verification_result.invalid?
|
57
|
+
end
|
58
|
+
|
59
|
+
it "fails when jwt is expired" do
|
60
|
+
auth0 = Auth0RS256JWTVerifier.new(
|
61
|
+
issuer: "https://multi-jobbers.eu.auth0.com/",
|
62
|
+
audience: "https://multijobbers.herokuapp.com/",
|
63
|
+
jwks_url: "https://multi-jobbers.eu.auth0.com/.well-known/jwks.json",
|
64
|
+
http: http_stub,
|
65
|
+
exp_verifier: exp_verifier_stub(expired: true),
|
66
|
+
)
|
67
|
+
|
68
|
+
verification_result = auth0.verify(
|
69
|
+
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5rTkJPRFEzUWpNeFF6WkVOa1" \
|
70
|
+
"kzUVVNMk9VTTFSVGMxUTBZMk4wUXdSVGRHTWpkRk9UQkROdyJ9.eyJpc3MiOiJodHRwczo" \
|
71
|
+
"vL211bHRpLWpvYmJlcnMuZXUuYXV0aDAuY29tLyIsInN1YiI6IjZwM2tFc0pkOUhteGFxV" \
|
72
|
+
"VIwdXN3c1VFRUdoZTNuQ05IQGNsaWVudHMiLCJhdWQiOiJodHRwczovL211bHRpam9iYmV" \
|
73
|
+
"ycy5oZXJva3VhcHAuY29tLyIsImV4cCI6MTQ5NzE4MTMzNSwiaWF0IjoxNDk3MDk0OTM1L" \
|
74
|
+
"CJzY29wZSI6IiJ9.LPtyDb26UxS5NKHEU2VJdmj7pDI4-tfgue2Ttk62H0a9XehsCArwwy" \
|
75
|
+
"QtI2ZAXxY8gQGS4dhXpcDqevmpfAy9zcjMEvvjWqmcGpepL8bn4MUJ_lAmL3A3FJXduf8T" \
|
76
|
+
"pHRXUHiMcdGT0vcFrv5kkMHDzTiwvOUxPcRT5nufX16Vqg3MTQS5pDb2NPcLCqI4PrJhse" \
|
77
|
+
"uJnDthxYelUvf6AIyVesuK5e3g8FLiXjZoPmwr3u6xeljF2KECetBPskKI8MgWrhIDD9Zv" \
|
78
|
+
"-O_fV1UZ41M-7zURcsQNYV--knHuX0i6nF46JlnbdqoA35d8LJvtnzbiO7hj8mP_GMa8FS" \
|
79
|
+
"cKqq5D8w",
|
80
|
+
)
|
81
|
+
|
82
|
+
refute verification_result.valid?
|
83
|
+
assert verification_result.invalid?
|
84
|
+
end
|
85
|
+
|
86
|
+
it "fails when jwt is random string" do
|
87
|
+
auth0 = Auth0RS256JWTVerifier.new(
|
88
|
+
issuer: "https://multi-jobbers.eu.auth0.com/",
|
89
|
+
audience: "https://multijobbers.herokuapp.com/",
|
90
|
+
jwks_url: "https://multi-jobbers.eu.auth0.com/.well-known/jwks.json",
|
91
|
+
http: http_stub,
|
92
|
+
exp_verifier: exp_verifier_stub(expired: true),
|
93
|
+
)
|
94
|
+
|
95
|
+
verification_result = auth0.verify("random string")
|
96
|
+
|
97
|
+
refute verification_result.valid?
|
98
|
+
assert verification_result.invalid?
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def http_stub
|
104
|
+
@http_stub = Object.new
|
105
|
+
jwks = sample_jwks
|
106
|
+
@http_stub.define_singleton_method(:get) { |*_| jwks }
|
107
|
+
@http_stub
|
108
|
+
end
|
109
|
+
|
110
|
+
def exp_verifier_stub(expired: false)
|
111
|
+
@exp_verifier_stub = Object.new
|
112
|
+
@exp_verifier_stub.define_singleton_method(:expired?) { |*_| expired }
|
113
|
+
@exp_verifier_stub
|
114
|
+
end
|
115
|
+
|
116
|
+
def sample_jwks
|
117
|
+
@sample_jwsk ||= File.read("./test/fixtures/sample_jwks.json")
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"keys":[
|
3
|
+
{
|
4
|
+
"alg":"RS256",
|
5
|
+
"kty":"RSA",
|
6
|
+
"use":"sig",
|
7
|
+
"x5c":[
|
8
|
+
"MIIDDzCCAfegAwIBAgIJXBxlkRndNs3ZMA0GCSqGSIb3DQEBCwUAMCUxIzAhBgNVBAMTGm11bHRpLWpvYmJlcnMuZXUuYXV0aDAuY29tMB4XDTE3MDUyOTEzNTA1OFoXDTMxMDIwNTEzNTA1OFowJTEjMCEGA1UEAxMabXVsdGktam9iYmVycy5ldS5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNwGcDVRbVjAXEhmMfa8OT37YAcFMhCGDZSgORTg/Wo+3E+WipgckLpT6+7LZH1ohgcbEVKkcDxjW0djYkSmgVvcDOxKClzj2iwXM0FYXJtU6Vwq1R5BzKWeMclbCZT7gEpVkCte3ZFvY80lYwlGvrw5QLMURnW5uMWA75Hf2VhzVx07M1KcwL3reHLd5YqDq5mrwTr+g5maFvl+t2dcMzOwuI2LhKgdImDGR6BUEteeEchNeir1r1lBrd5juwPiXT76ZVuGYnzf01UPqhGfcjSKey4W8WbIiYpDm7J0ORjGm/HAMHSvnOTYIqrJ62J0p5PexYzUGOhH0N06/G+VgtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFInchclYauappYScpEgOo+hUFwdZMA4GA1UdDwEB/wQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAoJ/uqy6TYmWMnuVP1vHQDv8pFRv5mpXa8D6aqDzfXKW3P4pAdFfDZ5PFVZkSiAnd/DJk//9cVAT7SEHHe9yxkp8N6MewxdHT+JGAszG4xyR73eV7gCAcITGh3t1efflNcxV7ycTJp61FuPtVs7MxdUAgWkpP5nMd41pHY01r5qy1g+ysUJ5TDI5SmqEAFc54etX5i8WUDtcH0DxJrib+YlZPA7xrbCV5DnykODXjJ0De7J89hcIXq4eL3TtbPCqJMPwH79cphJ9NUd+320WexTdNB5ysL7ML/Uk1KaWuS7fyA0C+gxSF6gOaJ3/u2OE3TrudLdTco/C87YnvWYJS9A=="
|
9
|
+
],
|
10
|
+
"n":"zcBnA1UW1YwFxIZjH2vDk9-2AHBTIQhg2UoDkU4P1qPtxPloqYHJC6U-vuy2R9aIYHGxFSpHA8Y1tHY2JEpoFb3AzsSgpc49osFzNBWFybVOlcKtUeQcylnjHJWwmU-4BKVZArXt2Rb2PNJWMJRr68OUCzFEZ1ubjFgO-R39lYc1cdOzNSnMC963hy3eWKg6uZq8E6_oOZmhb5frdnXDMzsLiNi4SoHSJgxkegVBLXnhHITXoq9a9ZQa3eY7sD4l0--mVbhmJ839NVD6oRn3I0insuFvFmyImKQ5uydDkYxpvxwDB0r5zk2CKqyetidKeT3sWM1BjoR9DdOvxvlYLQ",
|
11
|
+
"e":"AQAB",
|
12
|
+
"kid":"NkNBODQ3QjMxQzZENkY3QUM2OUM1RTc1Q0Y2N0QwRTdGMjdFOTBDNw",
|
13
|
+
"x5t":"NkNBODQ3QjMxQzZENkY3QUM2OUM1RTc1Q0Y2N0QwRTdGMjdFOTBDNw"
|
14
|
+
}
|
15
|
+
]
|
16
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"keys":[{"alg":"RS256","kty":"RSA","use":"sig","x5c":["MIIDETCCAfmgAwIBAgIJALUn2x16sU0BMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNVBAMMFGV4YW1wbGUuZXUuYXV0aDAuY29tMB4XDTE1MDQyMzA3MTcyOVoXDTI4MTIzMDA3MTcyOVowHzEdMBsGA1UEAwwUZXhhbXBsZS5ldS5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQColCyrzR24viQYf3gdHSrH1rCMS7wH3SOmzq5RRZeOxz6wcmTr8Jip/SQiS5QSsFfUoRk8PPbeafUhF+/NDwvCJXnUf8lTDeOAVRDM5hkfWnLZf8sBHYdAZXTps6Oz6Nq0MT4J/7cL43a1Q/UU8qwCYG652NPX2bPIAjfxq28MUJ47iz2EKg445MHpsuHU3MXqKApyLBlyUQL4VRw9xjlqkL45HTB2zCNuO4o8zQNE70jWQe8b4eauzLT5oeakUVTW5vGq5ryKE6T0vUERxYO/Bxzw+qfZ75IV9dQefUZ41WLB6PWP6OvzsRSEOGZR2LBMh3IfArhUwNxpkmTf4lZfAgMBAAGjUDBOMB0GA1UdDgQWBBShBM7FGFBWlZSI0AkqeI/tcZftQDAfBgNVHSMEGDAWgBShBM7FGFBWlZSI0AkqeI/tcZftQDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCOraDmW3eSauYY4Xdf81t7XgPiUiWaqHUOtLEFOVmpPlX8mHtcUpSHZwh2C52iLtFkBUq97r2AwmTArysTb+ZNsFotEaAxTe2f6dO8oV7lxjn5ApL3NZWVDXsO//F/kJTsA0AlGozdHedb/Xy90atY9IRBHqlY49QHJmiheSxy3QTuABEVfHShsbqhT22a/VOEWelwyJZyeQX03ysu6c9NqnWblU/9j/Ccg96VorL7bYN0dLgImdcZQFR8fjhGeV86n15C7ZvOE4cJyVdBc0er1IrEmx7oXG6YgxyBYMRL+DiIjZAftE6YjOuet2GQOSdGYPiYqn6Z7xLg4DPTaWEP"],"n":"qJQsq80duL4kGH94HR0qx9awjEu8B90jps6uUUWXjsc-sHJk6_CYqf0kIkuUErBX1KEZPDz23mn1IRfvzQ8LwiV51H_JUw3jgFUQzOYZH1py2X_LAR2HQGV06bOjs-jatDE-Cf-3C-N2tUP1FPKsAmBuudjT19mzyAI38atvDFCeO4s9hCoOOOTB6bLh1NzF6igKciwZclEC-FUcPcY5apC-OR0wdswjbjuKPM0DRO9I1kHvG-Hmrsy0-aHmpFFU1ubxqua8ihOk9L1BEcWDvwcc8Pqn2e-SFfXUHn1GeNViwej1j-jr87EUhDhmUdiwTIdyHwK4VMDcaZJk3-JWXw","e":"AQAB","kid":"QUVGRUVENTEwNDUwODlFQjA1QzE0QkVBMUY5NDFFRjFBRjI5Mzc3MA","x5t":"QUVGRUVENTEwNDUwODlFQjA1QzE0QkVBMUY5NDFFRjFBRjI5Mzc3MA"}]}
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: auth0_rs256_jwt_verifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Krzysztof Zielonka
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json-jwt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5'
|
69
|
+
description: Auth0 (https://auth0.com) is web service handling users identities which
|
70
|
+
can be easily plugged into your application. It provides SDKs for many languages
|
71
|
+
which enable you to sign up/in users and returns access token (JWT) in exchange.
|
72
|
+
Access token can be used then to access your's Web Service. This gem helps you to
|
73
|
+
verify (https://auth0.com/docs/api-auth/tutorials/verify-access-token#verify-the-signature)
|
74
|
+
such access token which has been signed using the RS256 algorithm.
|
75
|
+
email: krzysztof.zielonka@droidsonroids.pl
|
76
|
+
executables: []
|
77
|
+
extensions: []
|
78
|
+
extra_rdoc_files: []
|
79
|
+
files:
|
80
|
+
- ".gitignore"
|
81
|
+
- ".rubocop.yml"
|
82
|
+
- Gemfile
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- auth0_rs256_jwt_verifier.gemspec
|
86
|
+
- lib/auth0_access_tokens_rs256_verifier.rb
|
87
|
+
- lib/auth0_rs256_jwt_verifier.rb
|
88
|
+
- lib/auth0_rs256_jwt_verifier/certs_set.rb
|
89
|
+
- lib/auth0_rs256_jwt_verifier/exp_verifier.rb
|
90
|
+
- lib/auth0_rs256_jwt_verifier/jwk.rb
|
91
|
+
- lib/auth0_rs256_jwt_verifier/jwk_set_downloader.rb
|
92
|
+
- lib/auth0_rs256_jwt_verifier/jwt_decoder.rb
|
93
|
+
- lib/auth0_rs256_jwt_verifier/jwt_decoder_wrapper.rb
|
94
|
+
- lib/auth0_rs256_jwt_verifier/results.rb
|
95
|
+
- lib/auth0_rs256_jwt_verifier/user_id.rb
|
96
|
+
- lib/auth0_rs256_jwt_verifier/valid_jwk_set.rb
|
97
|
+
- test/.rubocop.yml
|
98
|
+
- test/auth0_rs256_jwt_verifier/jwt_decoder_test.rb
|
99
|
+
- test/auth0_rs256_jwt_verifier/jwt_decoder_wrapper_test.rb
|
100
|
+
- test/auth0_rs256_jwt_verifier_test.rb
|
101
|
+
- test/fixtures/sample_jwks.json
|
102
|
+
- test/fixtures/sample_jwks_2.json
|
103
|
+
- test/test_helper.rb
|
104
|
+
homepage: https://rubygems.org/gems/auth0_rs256_jwt_verifier
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.6.8
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Auth0 JWT (RS256) verification library
|
128
|
+
test_files: []
|