omniauth-krystal 1.2.0 → 1.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81124af273cae23b77ab10b6e278bcfe6b2968060949bdc7d13cad9989035afb
4
- data.tar.gz: 15556c4d72fbe57dedc461001ea39f5b86ae87b4ca723fbf5fc1756f7480ef6f
3
+ metadata.gz: f93afa35b415e64d9569aff71d3d19f65b4328fd70001535d26038d63c700aa7
4
+ data.tar.gz: f9e448d0d49a8f72354f6080881b25131cf1654f599b74bd446d7c16c1a09d1c
5
5
  SHA512:
6
- metadata.gz: 0e27564482e407b327c7d5744071499b1d8867dfa013b73df5f92304eb9ccf59908d873275a4de533c61c1b8c755107372ca5a5120cc0d8857d01913b475c3a9
7
- data.tar.gz: 4914bbd1e8f481e872acb7376499eee17386415d3e33e1314e779551374874ae6aa7cf7a604b6df812b6673fbc758ec1255011316e767431280458c5acefd76e
6
+ metadata.gz: b1fef42e9a4d45ea8ec48b3a7dc9e25a0beff33f4675ce307a8c90582fc0c53ce9b0d75cf2360dcb818189f0063573bea60e62ccb38a314b63250cb0084e4b95
7
+ data.tar.gz: c16b6c08c93432a84e70984163984baff7e87a19b92f00f512d60a0b28b19a6344ee75ffc922e10122d9acfaf98f3c813186fe99ff8e825d785de0c92aa687d3
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+ require 'omniauth/krystal/signing_keys'
5
+
6
+ module OmniAuth
7
+ module Krystal
8
+ class InitiatedLoginMiddleware
9
+ class Error < StandardError
10
+ end
11
+
12
+ class JWTDecodeError < Error
13
+ end
14
+
15
+ class JWTExpiredError < Error
16
+ end
17
+
18
+ class AntiReplayTokenAlreadyUsedError < Error
19
+ end
20
+
21
+ JWT_RESERVED_CLAIMS = %w[ar exp nbf iss aud jti iat sub].freeze
22
+
23
+ def initialize(app, options = {})
24
+ @app = app
25
+ @options = options
26
+
27
+ @options[:provider_name] ||= 'krystal'
28
+ @options[:identity_url] ||= ENV.fetch('KRYSTAL_IDENTITY_URL', 'https://identity.k.io')
29
+ @options[:anti_replay_expiry_seconds] ||= 60
30
+
31
+ @keys = SigningKeys.new("#{@options[:identity_url]}/.well-known/signing.json")
32
+ end
33
+
34
+ # rubocop:disable Metrics/MethodLength
35
+ # rubocop:disable Metrics/AbcSize
36
+ def call(env)
37
+ unless env['PATH_INFO'] == "/auth/#{@options[:provider_name]}/callback"
38
+ # If it's not a Krystal Identity auth callback then we don't
39
+ # need to do anything here.
40
+ return @app.call(env)
41
+ end
42
+
43
+ request = Rack::Request.new(env)
44
+ state = request.params['state']
45
+
46
+ if state.nil? || !state.start_with?('kidil_')
47
+ # Return to the app if the state is not a Krystal Identity
48
+ # initiated login.
49
+ return @app.call(env)
50
+ end
51
+
52
+ # Decode the JWT and ensure that the state is valid. JWT will check
53
+ # the expiry.
54
+ data = nil
55
+ begin
56
+ data, = JWT.decode(state.sub(/\Akidil_/, ''), nil, true, { algorithm: 'ES256', jwks: @keys })
57
+ rescue JWT::ExpiredSignature
58
+ raise JWTExpiredError, 'State parameter has expired'
59
+ rescue JWT::DecodeError
60
+ raise JWTDecodeError,
61
+ 'Invalid state parameter provided (either malformed, expired or signed with the wrong key)'
62
+ end
63
+
64
+ # Verify the replay token
65
+ verify_anti_replay_token(data['ar'])
66
+
67
+ # Set the expected omniauth state to the state that we have been given so it
68
+ # thinks the session is trusted as normal.
69
+ env['rack.session']['omniauth.state'] = state
70
+
71
+ # Set any additional params that were passed in the state.
72
+ env['rack.session']['omniauth.params'] = data.reject { |key| JWT_RESERVED_CLAIMS.include?(key) }
73
+
74
+ @app.call(env)
75
+ end
76
+ # rubocop:enable Metrics/AbcSize
77
+ # rubocop:enable Metrics/MethodLength
78
+
79
+ private
80
+
81
+ def verify_anti_replay_token(token)
82
+ return if @options[:redis].nil?
83
+
84
+ redis = @options[:redis]
85
+ key = "kidil-ar:#{token}"
86
+ if redis.get(key)
87
+ raise AntiReplayTokenAlreadyUsedError, 'Anti replay token has already been used'
88
+ end
89
+
90
+ redis.set(key,
91
+ Time.now.to_i,
92
+ nx: true, ex: @options[:anti_replay_expiry_seconds])
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'json'
6
+
7
+ module OmniAuth
8
+ module Krystal
9
+ class SigningKeys
10
+ attr_reader :cache, :cache_set_at
11
+
12
+ def initialize(url)
13
+ @url = url
14
+ @cache = nil
15
+ end
16
+
17
+ def call(_options)
18
+ invalidate_cache_if_appropriate
19
+ return @cache if @cache
20
+
21
+ download_jwks
22
+ @cache_set_at = Time.now.to_i
23
+ @cache
24
+ end
25
+
26
+ private
27
+
28
+ # Invalidate the cache if it's been more than 5 minutes since we last
29
+ # cached the data.
30
+ def invalidate_cache_if_appropriate
31
+ return if @cache_set_at && @cache_set_at >= (Time.now.to_i - 300)
32
+
33
+ @cache = nil
34
+ end
35
+
36
+ # rubocop:disable Metrics/AbcSize
37
+ def download_jwks
38
+ uri = URI.parse(@url)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ http.use_ssl = true if uri.scheme == 'https'
41
+ request = Net::HTTP::Get.new(uri.request_uri)
42
+ response = http.request(request)
43
+ raise Error, "Failed to download signing keys from #{@url}" if response.code != '200'
44
+
45
+ body = JSON.parse(response.body)
46
+
47
+ @cache = JWT::JWK::Set.new(body)
48
+ @cache.select! { |key| key[:use] == 'sig' }
49
+ @cache
50
+ end
51
+ # rubocop:enable Metrics/AbcSize
52
+ end
53
+ end
54
+ end
@@ -2,11 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module Krystal
5
- VERSION_FILE = File.expand_path('../../../VERSION', __dir__)
6
- VERSION = if File.file?(VERSION_FILE)
7
- File.read(VERSION_FILE).strip
8
- else
9
- '0.0.0'
10
- end
5
+ VERSION = '1.4.0'
11
6
  end
12
7
  end
@@ -38,6 +38,7 @@ module OmniAuth
38
38
  }
39
39
  end
40
40
 
41
+ # rubocop:disable Metrics/AbcSize
41
42
  def initialize(app, *args, &block)
42
43
  super
43
44
 
@@ -45,6 +46,7 @@ module OmniAuth
45
46
  options.client_options.authorize_url ||= "#{options.client_options.url}/oauth2/auth"
46
47
  options.client_options.token_url ||= "#{options.client_options.url}/oauth2/token"
47
48
  end
49
+ # rubocop:enable Metrics/AbcSize
48
50
 
49
51
  def scope
50
52
  access_token['scope']
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'omniauth/strategies/krystal'
4
+ require 'omniauth/krystal/initiated_login_middleware'
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-krystal
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Cooke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-22 00:00:00.000000000 Z
11
+ date: 2023-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
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: jwt
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'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: omniauth
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -46,6 +74,8 @@ extensions: []
46
74
  extra_rdoc_files: []
47
75
  files:
48
76
  - lib/omniauth-krystal.rb
77
+ - lib/omniauth/krystal/initiated_login_middleware.rb
78
+ - lib/omniauth/krystal/signing_keys.rb
49
79
  - lib/omniauth/krystal/version.rb
50
80
  - lib/omniauth/strategies/krystal.rb
51
81
  homepage: https://github.com/krystal/omniauth-krystal