authentic-rb 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
+ SHA256:
3
+ metadata.gz: 3b3df53a31e58a8aa456c9e9f7056921cbd361f9f68610fe0beaa50cea027ec1
4
+ data.tar.gz: e8c51fe75e10af3718d636bf5b501e8f8f9e82ff8bd1079a0dd58752dac4cbdf
5
+ SHA512:
6
+ metadata.gz: 7c67772174a1c70156c47f4acb9cfabdc7b1af5cedb9becd18db487fbc1b4ffd060d83b35bcb1c5c17dcbad4f52113bdea0e687cac0a439ffafd06b57be5f17c
7
+ data.tar.gz: 0333a4447268ccd350841ff55b72c7cdd48ebc1e164a8710484bb39f928837b752bd231b12453127ddc180d2640f411ee3c8840d4c31c194698195bfcd3174b6
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module Authentic
5
+ # Public: Represents a request error when requesting OIDC config or JWKs from authorization server.
6
+ class RequestError < StandardError
7
+ attr_reader :code
8
+ def initialize(msg, code)
9
+ @code = code
10
+ super(msg)
11
+ end
12
+ end
13
+
14
+ # Public: Represents an error with JWK.
15
+ class InvalidKey < StandardError; end
16
+
17
+ # Public: Represents a error with options passed to Authentic::Validator.
18
+ class IncompleteOptions < StandardError; end
19
+
20
+ # Public: Represents a bad JWT.
21
+ class InvalidToken < StandardError; end
22
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json/jwt'
4
+ require 'unirest'
5
+ require 'uri'
6
+ require 'authentic/key_store'
7
+
8
+ module Authentic
9
+ # Internal: manages JWK retrieval, caching, and validation.
10
+ class KeyManager
11
+ attr_reader :store, :well_known
12
+
13
+ def initialize(max_age)
14
+ @store = KeyStore.new(max_age || '10h')
15
+ @well_known = '/.well-known/openid-configuration'
16
+ end
17
+
18
+ # Public: retrieves JWK.
19
+ #
20
+ # jwt - JSON::JWT.
21
+ #
22
+ # Returns JSON::JWK.
23
+ def get(jwt)
24
+ iss = jwt.fetch(:iss)
25
+
26
+ result = store.get(iss, jwt.kid)
27
+
28
+ return result unless result.nil?
29
+
30
+ # Refresh all keys for an issuer while I have the updated data on hand
31
+ hydrate_iss_keys iss
32
+ store.get(iss, jwt.kid)
33
+ end
34
+
35
+ # Internal: validates RSA key.
36
+ #
37
+ # key - hash with key data.
38
+ #
39
+ # Returns boolean.
40
+ def valid_rsa_key(key)
41
+ key['use'] == 'sig' && key['kty'] == 'RSA' && key['kid']
42
+ end
43
+
44
+ # Internal: performs JSON request.
45
+ #
46
+ # key - hash with JWK data.
47
+ #
48
+ # Returns boolean.
49
+ def valid_key(key)
50
+ valid_rsa_key(key) && (key['x5c']&.length || (key['n'] && key['e']))
51
+ end
52
+
53
+ # Internal: performs JSON request.
54
+ #
55
+ # uri - endpoint to request.
56
+ #
57
+ # Returns JSON.
58
+ def json_req(uri)
59
+ resp = Unirest.get(uri, headers: { 'Accept' => 'application/json' })
60
+ raise RequestError.new("failed to retrieve JWK, status #{resp.code}", resp.code) unless (200..299).cover? resp.code
61
+
62
+ resp.body
63
+ end
64
+
65
+ # Internal: hydrates JWK cache.
66
+ #
67
+ # iss - issuer URI.
68
+ #
69
+ # Returns nothing.
70
+ def hydrate_iss_keys(iss)
71
+ uri = URI.join iss, well_known
72
+ json = json_req uri.to_s
73
+ body = json_req json['jwks_uri']
74
+
75
+ raise InvalidKey, "no valid JWK found, #{json['jwks_uri']}" if body['keys']&.blank?
76
+
77
+ keys = body['keys'].select { |key| valid_key(key) }
78
+ hydrate_store(keys, iss)
79
+ end
80
+
81
+ # Internal: hydrates key store.
82
+ #
83
+ # keys - array of keys hash.
84
+ # iss - JWT issuer endpoint.
85
+ #
86
+ # Returns nothing.
87
+ def hydrate_store(keys, iss)
88
+ keys.each do |key|
89
+ store.set(
90
+ iss, key['kid'],
91
+ JSON::JWK.new(
92
+ kty: key['kty'],
93
+ e: key['e'],
94
+ n: key['n'],
95
+ kid: key['kid']
96
+ )
97
+ )
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'authentic/oidc_key'
4
+
5
+ # nodoc
6
+ module Authentic
7
+ # Internal: Key store for caching JWKs.
8
+ class KeyStore
9
+ # Public: cache data
10
+ attr_reader :data, :max_age, :max_age_seconds
11
+
12
+ def initialize(max_age, data = {})
13
+ @data = data
14
+ @max_age = max_age
15
+ @max_age_seconds = human_time_to_seconds
16
+ end
17
+
18
+ # Public: Sets data, and wraps it in OIDCKey class if not presented as that type.
19
+ #
20
+ # iss - issuer
21
+ # kid - key id
22
+ #
23
+ # Returns JSON::JWK
24
+ def get(iss, kid)
25
+ key = get_key(iss, kid)
26
+ expires!(key)
27
+ data[key]&.value
28
+ end
29
+
30
+ # Internal: builds cache key
31
+ #
32
+ # iss - issuer
33
+ # kid - key id
34
+ #
35
+ # Returns string
36
+ def get_key(iss, kid)
37
+ "#{iss}/#{kid}"
38
+ end
39
+
40
+ # Public: Sets data, and wraps it in OIDCKey class if not presented as that type.
41
+ #
42
+ # iss - issuer
43
+ # kid - key id
44
+ # data - data to cache which is usually a single OIDC public key.
45
+ #
46
+ # Returns JSON::JWK
47
+ def set(iss, kid, new_data)
48
+ key = get_key(iss, kid)
49
+ data[key] = new_data.is_a?(OIDCKey) ? new_data : OIDCKey.new(new_data, max_age_seconds)
50
+ get(iss, kid)
51
+ end
52
+
53
+ # Internal: Verifies if data is expired and unset it
54
+ def expires!(key)
55
+ unset(key) if data[key]&.expired?
56
+ end
57
+
58
+ # Internal: deletes data from cache
59
+ def unset(key)
60
+ data.delete(key)
61
+ end
62
+
63
+ # frozen_string_literal: true
64
+
65
+ # Internal: converts human time to seconds for consumption of the cache service. Format ``
66
+ #
67
+ # human_time - represents time in hours, minutes, and seconds.
68
+ #
69
+ # Returns seconds.
70
+ def human_time_to_seconds
71
+ m = /(?:(\d*)h)?\s?(?:(\d*)?m)?\s?(?:(\d*)?s)?/.match(max_age)
72
+ h = ((m[1].to_i || 0) * 60) * 60
73
+ mi = (m[2].to_i || 0) * 60
74
+ s = (m[3].to_i || 0)
75
+ h + mi + s
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module Authentic
5
+ # Internal: JWKs cache data.
6
+ class OIDCKey
7
+ attr_reader :expires, :value
8
+
9
+ def initialize(value, max_age_seconds)
10
+ @value = value
11
+ @expires = max_age_seconds.nil? ? nil : Time.now.utc + max_age_seconds
12
+ end
13
+
14
+ def expired?
15
+ !expires.nil? && Time.now.utc > expires
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'authentic/error'
4
+ require 'authentic/key_manager'
5
+
6
+ # Public: proper validation of JWTs against JWKs.
7
+ module Authentic
8
+ # Public: validate JWTs against JWKs using iss whitelist in an environment variable.
9
+ #
10
+ # token - raw JWT.
11
+ #
12
+ # Returns boolean.
13
+ def self.valid?(token)
14
+ Validator.new.valid?(token)
15
+ end
16
+
17
+ # Public: uses environment variable for iss whitelist and validates JWT,
18
+ # raises an error for invalid JWTs, errors requesting JWKs, the lack of valid JWKs, or non white listed ISS.
19
+ #
20
+ # token - raw JWT.
21
+ #
22
+ # Returns nothing.
23
+ def self.ensure_valid(token)
24
+ Validator.new.ensure_valid(token)
25
+ end
26
+
27
+ # Public: validates JWTs against JWKs.
28
+ class Validator
29
+ attr_reader :iss_whitelist, :manager, :opts
30
+
31
+ def initialize(options = {})
32
+ @opts = options
33
+ @iss_whitelist = opts.fetch(:iss_whitelist) { ENV['AUTHENTIC_ISS_WHITELIST']&.split(',') }
34
+ valid_opts = !iss_whitelist&.empty?
35
+ raise IncompleteOptions unless valid_opts
36
+
37
+ @manager = KeyManager.new opts[:cache_max_age]
38
+ end
39
+
40
+ # Public: validates JWT, returns true if valid, false if not.
41
+ #
42
+ # token - raw JWT.
43
+ #
44
+ # Returns boolean.
45
+ def valid?(token)
46
+ ensure_valid token
47
+ true
48
+ rescue InvalidToken, InvalidKey, RequestError
49
+ false
50
+ end
51
+
52
+ # Public: validates JWT, raises an error for invalid JWTs, errors requesting JWKs,
53
+ # the lack of valid JWKs, or non white listed ISS.
54
+ #
55
+ # token - raw JWT.
56
+ #
57
+ # Returns nothing.
58
+ def ensure_valid(token)
59
+ jwt = decode_jwt token
60
+
61
+ begin
62
+ key = manager.get jwt
63
+
64
+ # Slightly more accurate to raise a key error here for nil key,
65
+ # rather then verify raising an error that would lead to InvalidToken
66
+ raise InvalidKey, 'invalid JWK' if key.nil?
67
+
68
+ jwt.verify! key
69
+ rescue JSON::JWT::UnexpectedAlgorithm, JSON::JWT::VerificationFailed
70
+ raise InvalidToken, 'failed to validate token against JWK'
71
+ rescue OpenSSL::PKey::PKeyError
72
+ raise InvalidKey, 'invalid JWK'
73
+ end
74
+ end
75
+
76
+ # Decodes and does basic validation of JWT.
77
+ #
78
+ # token - raw JWT.
79
+ #
80
+ # Returns JSON::JWT
81
+ def decode_jwt(token)
82
+ raise InvalidToken, 'invalid nil JWT provided' unless token
83
+
84
+ JSON::JWT.decode(token, :skip_verification).tap do |jwt|
85
+ raise InvalidToken, 'JWT iss was not located in provided whitelist' unless iss_whitelist.index jwt[:iss]
86
+ end
87
+ rescue JSON::JWT::InvalidFormat
88
+ raise InvalidToken, 'invalid JWT format'
89
+ end
90
+ end
91
+ end
data/lib/authentic.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'authentic/validator'
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: authentic-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Articulate
8
+ - Andy Gertjejansen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-11-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json-jwt
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.9'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.9.4
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '1.9'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.9.4
34
+ - !ruby/object:Gem::Dependency
35
+ name: unirest
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.2
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: 1.1.2
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 1.1.2
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 1.1.2
54
+ description: Ruby toolkit for Auth0 API https://auth0.com.
55
+ email:
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - lib/authentic.rb
61
+ - lib/authentic/error.rb
62
+ - lib/authentic/key_manager.rb
63
+ - lib/authentic/key_store.rb
64
+ - lib/authentic/oidc_key.rb
65
+ - lib/authentic/validator.rb
66
+ homepage: https://rubygems.org/gems/authentic-rb
67
+ licenses: []
68
+ metadata:
69
+ source_code_uri: https://github.com/articulate/authentic-rb
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.7.8
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: validation of JWTs against JWKs
90
+ test_files: []