omniauth-krystal 1.1.3 → 1.3.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: f64140c4b23240abc668a188f8a6ea105859459f097cfb2aa99db58ef527e20b
4
- data.tar.gz: 7c157cc63737b8ca5f2d96261f0724adf195921c35477cf6d6bbedd0993abd1c
3
+ metadata.gz: ff1b48f554292417ddac46a620f11567455881410467ab639f74fa5eb87e2628
4
+ data.tar.gz: 0dfc2595766ef70626614953fcd08539dbe594e457ed5fcdc8efb336f5f1fce9
5
5
  SHA512:
6
- metadata.gz: 8a2f572a95e3f70fd85c5441253ad44fac34c6ece74e90ab61ae02f4171d8f925c019d683e9c833d56de36c1a743af3eb00fdd7ee61f72c358da69cbf450a121
7
- data.tar.gz: 606e3760ab86c72f57df206a93624a1d48b7a666714dcd8199a20befd5bc56d5eab1a2e746f21704e9034d95ed5ffafb0d0e731c5a4a1fe776bfd49713e921c6
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
@@ -8,9 +8,10 @@ module OmniAuth
8
8
  option :name, 'krystal'
9
9
 
10
10
  option :client_options,
11
- site: ENV.fetch('KRYSTAL_IDENTITY_API_URL', 'https://identity.k.io/api/v1'),
12
- authorize_url: ENV.fetch('KRYSTAL_IDENTITY_OAUTH_AUTHORIZE_URL', 'https://identity.k.io/oauth2/auth'),
13
- token_url: ENV.fetch('KRYSTAL_IDENTITY_OAUTH_TOKEN_URL', 'https://identity.k.io/oauth2/token')
11
+ url: ENV.fetch('KRYSTAL_IDENTITY_URL', 'https://identity.k.io'),
12
+ site: ENV.fetch('KRYSTAL_IDENTITY_API_URL', nil),
13
+ authorize_url: ENV.fetch('KRYSTAL_IDENTITY_OAUTH_AUTHORIZE_URL', nil),
14
+ token_url: ENV.fetch('KRYSTAL_IDENTITY_OAUTH_TOKEN_URL', nil)
14
15
 
15
16
  option :authorize_params,
16
17
  scope: 'user.profile'
@@ -27,10 +28,26 @@ module OmniAuth
27
28
  extra do
28
29
  {
29
30
  raw_info: raw_info,
30
- scope: scope
31
+ scope: scope,
32
+ session_id: raw_info['session_id'],
33
+ first_name: raw_info['user']['first_name'],
34
+ last_name: raw_info['user']['last_name'],
35
+ email_addresses: raw_info['user']['email_addresses'],
36
+ roles: raw_info['user']['roles'],
37
+ two_factor_auth_enabled: raw_info['user']['two_factor_auth_enabled']
31
38
  }
32
39
  end
33
40
 
41
+ # rubocop:disable Metrics/AbcSize
42
+ def initialize(app, *args, &block)
43
+ super
44
+
45
+ options.client_options.site ||= "#{options.client_options.url}/api/v1"
46
+ options.client_options.authorize_url ||= "#{options.client_options.url}/oauth2/auth"
47
+ options.client_options.token_url ||= "#{options.client_options.url}/oauth2/token"
48
+ end
49
+ # rubocop:enable Metrics/AbcSize
50
+
34
51
  def scope
35
52
  access_token['scope']
36
53
  end
@@ -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.1.3
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-04-18 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