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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f93afa35b415e64d9569aff71d3d19f65b4328fd70001535d26038d63c700aa7
|
4
|
+
data.tar.gz: f9e448d0d49a8f72354f6080881b25131cf1654f599b74bd446d7c16c1a09d1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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']
|
data/lib/omniauth-krystal.rb
CHANGED
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.
|
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
|
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
|