omniauth-krystal 1.2.0 → 1.3.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: ff1b48f554292417ddac46a620f11567455881410467ab639f74fa5eb87e2628
4
+ data.tar.gz: 0dfc2595766ef70626614953fcd08539dbe594e457ed5fcdc8efb336f5f1fce9
5
5
  SHA512:
6
- metadata.gz: 0e27564482e407b327c7d5744071499b1d8867dfa013b73df5f92304eb9ccf59908d873275a4de533c61c1b8c755107372ca5a5120cc0d8857d01913b475c3a9
7
- data.tar.gz: 4914bbd1e8f481e872acb7376499eee17386415d3e33e1314e779551374874ae6aa7cf7a604b6df812b6673fbc758ec1255011316e767431280458c5acefd76e
6
+ metadata.gz: 95ed948cb99326e1b8917b06ce87d25133570d1162206eac4310975e7271b033f7b4e7a734a86a6a9949d2e06737e84c77b14fc841ea58f76a5bc703e509d1b8
7
+ data.tar.gz: 2970ce91eaa7e31c94a877f734fe5413ad21b1f5166c7c3628b7a1b0489315088c776b50e6b3fd0bf367db7928fa1ea63d58521661567954fdfa195bedfe5106
@@ -0,0 +1,91 @@
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
+ def initialize(app, options = {})
22
+ @app = app
23
+ @options = options
24
+
25
+ @options[:provider_name] ||= 'krystal'
26
+ @options[:identity_url] ||= ENV.fetch('KRYSTAL_IDENTITY_URL', 'https://identity.k.io')
27
+ @options[:anti_replay_expiry_seconds] ||= 60
28
+
29
+ @keys = SigningKeys.new("#{@options[:identity_url]}/.well-known/signing.json")
30
+ end
31
+
32
+ # rubocop:disable Metrics/MethodLength
33
+ # rubocop:disable Metrics/AbcSize
34
+ def call(env)
35
+ unless env['PATH_INFO'] == "/auth/#{@options[:provider_name]}/callback"
36
+ # If it's not a Krystal Identity auth callback then we don't
37
+ # need to do anything here.
38
+ return @app.call(env)
39
+ end
40
+
41
+ request = Rack::Request.new(env)
42
+ state = request.params['state']
43
+
44
+ if state.nil? || !state.start_with?('kidil_')
45
+ # Return to the app if the state is not a Krystal Identity
46
+ # initiated login.
47
+ return @app.call(env)
48
+ end
49
+
50
+ # Decode the JWT and ensure that the state is valid. JWT will check
51
+ # the expiry.
52
+ data = nil
53
+ begin
54
+ data, = JWT.decode(state.sub(/\Akidil_/, ''), nil, true, { algorithm: 'ES256', jwks: @keys })
55
+ rescue JWT::ExpiredSignature
56
+ raise JWTExpiredError, 'State parameter has expired'
57
+ rescue JWT::DecodeError
58
+ raise JWTDecodeError,
59
+ 'Invalid state parameter provided (either malformed, expired or signed with the wrong key)'
60
+ end
61
+
62
+ # Verify the replay token
63
+ verify_anti_replay_token(data['ar'])
64
+
65
+ # Set the expected omniauth state to the state that we have been given so it
66
+ # thinks the session is trusted as normal.
67
+ env['rack.session']['omniauth.state'] = state
68
+
69
+ @app.call(env)
70
+ end
71
+ # rubocop:enable Metrics/AbcSize
72
+ # rubocop:enable Metrics/MethodLength
73
+
74
+ private
75
+
76
+ def verify_anti_replay_token(token)
77
+ return if @options[:redis].nil?
78
+
79
+ redis = @options[:redis]
80
+ key = "kidil-ar:#{token}"
81
+ if redis.get(key)
82
+ raise AntiReplayTokenAlreadyUsedError, 'Anti replay token has already been used'
83
+ end
84
+
85
+ redis.set(key,
86
+ Time.now.to_i,
87
+ nx: true, ex: @options[:anti_replay_expiry_seconds])
88
+ end
89
+ end
90
+ end
91
+ 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.3.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.3.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-05-29 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