omniauth-krystal 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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