himari 0.4.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +58 -0
- data/lib/himari/access_token.rb +77 -4
- data/lib/himari/access_token_jwt.rb +46 -0
- data/lib/himari/app.rb +101 -28
- data/lib/himari/authorization_code.rb +18 -4
- data/lib/himari/client_registration.rb +71 -4
- data/lib/himari/config.rb +8 -3
- data/lib/himari/decisions/authentication.rb +18 -2
- data/lib/himari/decisions/authorization.rb +18 -7
- data/lib/himari/decisions/base.rb +7 -3
- data/lib/himari/decisions/claims.rb +14 -9
- data/lib/himari/dynamic_client_registration.rb +255 -0
- data/lib/himari/id_token.rb +15 -28
- data/lib/himari/item_provider.rb +3 -1
- data/lib/himari/item_providers/oauth_client_metadata.rb +222 -0
- data/lib/himari/item_providers/static.rb +2 -0
- data/lib/himari/item_providers/storage.rb +33 -0
- data/lib/himari/jwt_token.rb +50 -0
- data/lib/himari/lifetime_value.rb +5 -3
- data/lib/himari/log_line.rb +2 -0
- data/lib/himari/middlewares/authentication_rule.rb +2 -0
- data/lib/himari/middlewares/authorization_rule.rb +2 -0
- data/lib/himari/middlewares/claims_rule.rb +2 -0
- data/lib/himari/middlewares/client.rb +2 -0
- data/lib/himari/middlewares/config.rb +2 -0
- data/lib/himari/middlewares/dynamic_clients.rb +55 -0
- data/lib/himari/middlewares/metadata_clients.rb +121 -0
- data/lib/himari/middlewares/signing_key.rb +2 -0
- data/lib/himari/provider_chain.rb +3 -1
- data/lib/himari/refresh_token.rb +93 -0
- data/lib/himari/rule.rb +2 -0
- data/lib/himari/rule_processor.rb +3 -0
- data/lib/himari/services/client_registration_endpoint.rb +78 -0
- data/lib/himari/services/downstream_authorization.rb +22 -7
- data/lib/himari/services/jwks_endpoint.rb +3 -1
- data/lib/himari/services/oidc_authorization_endpoint.rb +56 -3
- data/lib/himari/services/oidc_provider_metadata_endpoint.rb +30 -7
- data/lib/himari/services/oidc_token_endpoint.rb +225 -38
- data/lib/himari/services/oidc_userinfo_endpoint.rb +14 -8
- data/lib/himari/services/upstream_authentication.rb +62 -14
- data/lib/himari/session_data.rb +31 -2
- data/lib/himari/signing_key.rb +17 -14
- data/lib/himari/storages/base.rb +45 -1
- data/lib/himari/storages/filesystem.rb +14 -3
- data/lib/himari/storages/memory.rb +10 -2
- data/lib/himari/token_string.rb +40 -4
- data/lib/himari/version.rb +1 -1
- data/public/public/index.css +18 -0
- data/views/consent.erb +59 -0
- metadata +49 -14
data/lib/himari/config.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'logger'
|
|
2
4
|
require 'time'
|
|
3
5
|
require 'json'
|
|
@@ -5,7 +7,7 @@ require 'himari/log_line'
|
|
|
5
7
|
|
|
6
8
|
module Himari
|
|
7
9
|
class Config
|
|
8
|
-
def initialize(issuer:, storage:, providers: [], log_output: $stdout, log_level: Logger::INFO, preserve_rack_logger: false, custom_templates: {}, custom_messages: {}, release_fragment: nil)
|
|
10
|
+
def initialize(issuer:, storage:, providers: [], log_output: $stdout, log_level: Logger::INFO, preserve_rack_logger: false, custom_templates: {}, custom_messages: {}, release_fragment: nil, scopes_supported: [], claims_supported: [])
|
|
9
11
|
@issuer = issuer
|
|
10
12
|
@providers = providers
|
|
11
13
|
@storage = storage
|
|
@@ -17,14 +19,17 @@ module Himari
|
|
|
17
19
|
@custom_messages = custom_messages
|
|
18
20
|
@custom_templates = custom_templates
|
|
19
21
|
@release_fragment = release_fragment
|
|
22
|
+
|
|
23
|
+
@scopes_supported = scopes_supported
|
|
24
|
+
@claims_supported = claims_supported
|
|
20
25
|
end
|
|
21
26
|
|
|
22
|
-
attr_reader :issuer, :providers, :storage, :preserve_rack_logger, :custom_messages, :custom_templates, :release_fragment
|
|
27
|
+
attr_reader :issuer, :providers, :storage, :preserve_rack_logger, :custom_messages, :custom_templates, :release_fragment, :scopes_supported, :claims_supported
|
|
23
28
|
|
|
24
29
|
def logger
|
|
25
30
|
@logger ||= Logger.new(@log_output).tap do |l|
|
|
26
31
|
l.level = @log_level
|
|
27
|
-
l.formatter = proc do |severity, datetime,
|
|
32
|
+
l.formatter = proc do |severity, datetime, _progname, msg|
|
|
28
33
|
log = {time: datetime.xmlschema, severity: severity.to_s, pid: Process.pid}
|
|
29
34
|
|
|
30
35
|
case msg
|
|
@@ -1,15 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'himari/decisions/base'
|
|
2
4
|
require 'himari/session_data'
|
|
3
5
|
|
|
4
6
|
module Himari
|
|
5
7
|
module Decisions
|
|
6
8
|
class Authentication < Base
|
|
7
|
-
Context = Struct.new(:provider, :claims, :user_data, :request, keyword_init: true)
|
|
9
|
+
Context = Struct.new(:provider, :claims, :user_data, :request, :grant_type, :refresh_info, keyword_init: true) do
|
|
10
|
+
def initial?; grant_type.nil? || grant_type == :initial; end
|
|
11
|
+
def refresh?; grant_type == :refresh_token; end
|
|
12
|
+
end
|
|
8
13
|
|
|
9
14
|
allow_effects(:allow, :deny, :skip)
|
|
10
15
|
|
|
16
|
+
def initialize(refresh_info: nil)
|
|
17
|
+
super()
|
|
18
|
+
@refresh_info = refresh_info
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_accessor :refresh_info
|
|
22
|
+
|
|
11
23
|
def to_evolve_args
|
|
12
|
-
{}
|
|
24
|
+
{refresh_info: @refresh_info}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def as_log
|
|
28
|
+
to_h.merge(refresh_info_set: !@refresh_info.nil?)
|
|
13
29
|
end
|
|
14
30
|
end
|
|
15
31
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'himari/decisions/base'
|
|
2
4
|
require 'himari/lifetime_value'
|
|
3
5
|
|
|
@@ -16,26 +18,34 @@ module Himari
|
|
|
16
18
|
email_verified
|
|
17
19
|
)
|
|
18
20
|
|
|
19
|
-
Context = Struct.new(:claims, :user_data, :request, :client, keyword_init: true)
|
|
21
|
+
Context = Struct.new(:claims, :user_data, :request, :client, :scopes, :grant_type, keyword_init: true) do
|
|
22
|
+
def initial?; grant_type.nil? || grant_type == :initial; end
|
|
23
|
+
def refresh?; grant_type == :refresh_token; end
|
|
24
|
+
end
|
|
20
25
|
|
|
21
26
|
allow_effects(:allow, :deny, :continue, :skip)
|
|
22
27
|
|
|
23
|
-
def initialize(claims: {}, allowed_claims: DEFAULT_ALLOWED_CLAIMS, lifetime: 3600)
|
|
28
|
+
def initialize(claims: {}, allowed_claims: DEFAULT_ALLOWED_CLAIMS, lifetime: 3600, mint_jwt_access_token: false)
|
|
24
29
|
super()
|
|
25
30
|
@claims = claims
|
|
26
31
|
@allowed_claims = allowed_claims
|
|
32
|
+
@mint_jwt_access_token = mint_jwt_access_token
|
|
27
33
|
self.lifetime = lifetime
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
attr_reader :claims, :allowed_claims
|
|
31
37
|
attr_reader :lifetime
|
|
32
38
|
|
|
39
|
+
# When set by an authz rule, the issued access token is an RFC 9068 JWT instead of an
|
|
40
|
+
# opaque token (the token is still tracked and validated against storage either way).
|
|
41
|
+
attr_accessor :mint_jwt_access_token
|
|
42
|
+
|
|
33
43
|
def lifetime=(x)
|
|
34
|
-
case x
|
|
44
|
+
@lifetime = case x
|
|
35
45
|
when LifetimeValue
|
|
36
|
-
|
|
46
|
+
x
|
|
37
47
|
else
|
|
38
|
-
|
|
48
|
+
LifetimeValue.from_integer(x)
|
|
39
49
|
end
|
|
40
50
|
end
|
|
41
51
|
|
|
@@ -44,15 +54,16 @@ module Himari
|
|
|
44
54
|
claims: @claims.dup,
|
|
45
55
|
allowed_claims: @allowed_claims.dup,
|
|
46
56
|
lifetime: @lifetime,
|
|
57
|
+
mint_jwt_access_token: @mint_jwt_access_token,
|
|
47
58
|
}
|
|
48
59
|
end
|
|
49
60
|
|
|
50
61
|
def as_log
|
|
51
|
-
to_h.merge(claims: output_claims, lifetime: @lifetime.to_h)
|
|
62
|
+
to_h.merge(claims: output_claims, lifetime: @lifetime.to_h, mint_jwt_access_token: @mint_jwt_access_token)
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
def output_claims
|
|
55
|
-
claims.select { |k,_v| allowed_claims.include?(k) }
|
|
66
|
+
claims.select { |k, _v| allowed_claims.include?(k) }
|
|
56
67
|
end
|
|
57
68
|
end
|
|
58
69
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Himari
|
|
2
4
|
module Decisions
|
|
3
5
|
class Base
|
|
@@ -8,8 +10,8 @@ module Himari
|
|
|
8
10
|
@valid_effects = effects
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :valid_effects
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def initialize
|
|
@@ -45,6 +47,7 @@ module Himari
|
|
|
45
47
|
|
|
46
48
|
def set_rule_name(rule_name)
|
|
47
49
|
raise "cannot override rule_name" if @rule_name
|
|
50
|
+
|
|
48
51
|
@rule_name = rule_name
|
|
49
52
|
self
|
|
50
53
|
end
|
|
@@ -52,7 +55,8 @@ module Himari
|
|
|
52
55
|
def decide!(effect, comment = "", user_facing_message: nil, suggest: nil)
|
|
53
56
|
raise DecisionAlreadyMade, "decision can only be made once per rule (#{rule_name})" if @effect
|
|
54
57
|
raise InvalidEffect, "this effect is not valid under this rule. Valid effects: #{self.class.valid_effects.inspect} (#{rule_name})" unless self.class.valid_effects.include?(effect)
|
|
55
|
-
raise InvalidEffect, "only deny effect can have suggestion" if suggest&& effect != :deny
|
|
58
|
+
raise InvalidEffect, "only deny effect can have suggestion" if suggest && effect != :deny
|
|
59
|
+
|
|
56
60
|
@effect = effect
|
|
57
61
|
@effect_comment = comment
|
|
58
62
|
@effect_user_facing_message = user_facing_message
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'himari/decisions/base'
|
|
2
4
|
require 'himari/session_data'
|
|
3
5
|
|
|
@@ -7,31 +9,34 @@ module Himari
|
|
|
7
9
|
class UninitializedError < StandardError; end
|
|
8
10
|
class AlreadyInitializedError < StandardError; end
|
|
9
11
|
|
|
10
|
-
Context = Struct.new(:request, :auth, keyword_init: true) do
|
|
11
|
-
def
|
|
12
|
+
Context = Struct.new(:request, :auth, :provider, :grant_type, :refresh_info, keyword_init: true) do
|
|
13
|
+
def initial?; grant_type.nil? || grant_type == :initial; end
|
|
14
|
+
def refresh?; grant_type == :refresh_token; end
|
|
12
15
|
end
|
|
13
16
|
|
|
14
|
-
allow_effects(:continue, :skip)
|
|
17
|
+
allow_effects(:continue, :skip, :deny)
|
|
15
18
|
|
|
16
|
-
def initialize(claims: nil, user_data: nil, lifetime: nil)
|
|
19
|
+
def initialize(claims: nil, user_data: nil, lifetime: nil, refresh_info: nil)
|
|
17
20
|
super()
|
|
18
21
|
@claims = claims
|
|
19
22
|
@user_data = user_data
|
|
20
23
|
@lifetime = lifetime
|
|
24
|
+
@refresh_info = refresh_info
|
|
21
25
|
end
|
|
22
26
|
|
|
23
|
-
attr_accessor :lifetime
|
|
27
|
+
attr_accessor :lifetime, :refresh_info
|
|
24
28
|
|
|
25
29
|
def to_evolve_args
|
|
26
30
|
{
|
|
27
31
|
claims: @claims.dup,
|
|
28
32
|
user_data: @user_data.dup,
|
|
29
33
|
lifetime: @lifetime&.to_i,
|
|
34
|
+
refresh_info: @refresh_info,
|
|
30
35
|
}
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def as_log
|
|
34
|
-
to_h.merge(claims: @claims)
|
|
39
|
+
to_h.merge(claims: @claims, refresh_info_set: !@refresh_info.nil?)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def output
|
|
@@ -42,14 +47,14 @@ module Himari
|
|
|
42
47
|
if @claims
|
|
43
48
|
raise AlreadyInitializedError, "Claims already initialized; use decision.claims to make modification, or rule might be behaving wrong"
|
|
44
49
|
end
|
|
50
|
+
|
|
45
51
|
@claims = claims.dup
|
|
46
52
|
@user_data = {}
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def claims
|
|
50
|
-
unless @claims
|
|
51
|
-
|
|
52
|
-
end
|
|
56
|
+
raise UninitializedError, "Claims uninitialized; use decision.initialize_claims! to declare claims first (or rule order might be unintentional)" unless @claims
|
|
57
|
+
|
|
53
58
|
@claims
|
|
54
59
|
end
|
|
55
60
|
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/sha2'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'addressable/uri'
|
|
6
|
+
require 'himari/client_registration'
|
|
7
|
+
|
|
8
|
+
module Himari
|
|
9
|
+
# A client created at runtime via RFC 7591 Dynamic Client Registration, persisted in
|
|
10
|
+
# storage. This is purely a storage/registration record: the registration endpoint
|
|
11
|
+
# interacts with it directly, while the OIDC endpoints only ever see the plain
|
|
12
|
+
# ClientRegistration produced by #to_client_registration at the provider layer.
|
|
13
|
+
class DynamicClientRegistration
|
|
14
|
+
# Default registration lifetime; overridable per deployment via Middlewares::DynamicClients.
|
|
15
|
+
REGISTRATION_LIFETIME = 180 * 86400
|
|
16
|
+
|
|
17
|
+
SUPPORTED_GRANT_TYPES = %w(authorization_code refresh_token).freeze
|
|
18
|
+
SUPPORTED_RESPONSE_TYPES = %w(code).freeze
|
|
19
|
+
SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS = %w(none client_secret_basic client_secret_post).freeze
|
|
20
|
+
|
|
21
|
+
DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD = 'client_secret_basic'
|
|
22
|
+
|
|
23
|
+
MAX_REDIRECT_URIS = 32
|
|
24
|
+
MAX_URI_LENGTH = 2000
|
|
25
|
+
MAX_CLIENT_NAME_LENGTH = 60
|
|
26
|
+
DANGEROUS_REDIRECT_URI_SCHEMES = %w(javascript data vbscript file blob).freeze
|
|
27
|
+
|
|
28
|
+
# Raised on invalid client metadata. error_code maps to an RFC 7591 §3.2.2 error code.
|
|
29
|
+
class ValidationError < StandardError
|
|
30
|
+
def initialize(error_code, message)
|
|
31
|
+
@error_code = error_code
|
|
32
|
+
super(message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :error_code
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build and validate a registration from RFC 7591 client metadata.
|
|
39
|
+
#
|
|
40
|
+
# @param metadata [Hash] parsed client metadata (symbolized keys) from the request body
|
|
41
|
+
# @return [DynamicClientRegistration]
|
|
42
|
+
def self.register(metadata:, registration_ip: nil, registration_remote_addr: nil, registration_x_forwarded_for: nil, lifetime: REGISTRATION_LIFETIME, ignore_localhost_redirect_uri_port: true, now: Time.now)
|
|
43
|
+
raise ValidationError.new(:invalid_client_metadata, 'request body must be a JSON object') unless metadata.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
auth_method = metadata.fetch(:token_endpoint_auth_method, DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD).to_s
|
|
46
|
+
unless SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS.include?(auth_method)
|
|
47
|
+
raise ValidationError.new(:invalid_client_metadata, "unsupported token_endpoint_auth_method: #{auth_method}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
grant_types = Array(metadata[:grant_types] || %w(authorization_code)).map(&:to_s)
|
|
51
|
+
unless (grant_types - SUPPORTED_GRANT_TYPES).empty?
|
|
52
|
+
raise ValidationError.new(:invalid_client_metadata, "unsupported grant_types: #{(grant_types - SUPPORTED_GRANT_TYPES).join(",")}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
response_types = Array(metadata[:response_types] || %w(code)).map(&:to_s)
|
|
56
|
+
unless (response_types - SUPPORTED_RESPONSE_TYPES).empty?
|
|
57
|
+
raise ValidationError.new(:invalid_client_metadata, "unsupported response_types: #{(response_types - SUPPORTED_RESPONSE_TYPES).join(",")}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if response_types.include?('code') && !grant_types.include?('authorization_code')
|
|
61
|
+
raise ValidationError.new(:invalid_client_metadata, 'response_type "code" requires grant_type "authorization_code"')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
redirect_uris = validate_redirect_uris(metadata[:redirect_uris])
|
|
65
|
+
|
|
66
|
+
client_name = metadata[:client_name]&.to_s
|
|
67
|
+
if client_name && client_name.length > MAX_CLIENT_NAME_LENGTH
|
|
68
|
+
raise ValidationError.new(:invalid_client_metadata, "client_name must not exceed #{MAX_CLIENT_NAME_LENGTH} characters")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
client_uri = validate_client_uri(metadata[:client_uri])
|
|
72
|
+
|
|
73
|
+
issued_at = now.to_i
|
|
74
|
+
secret = auth_method == 'none' ? nil : SecureRandom.urlsafe_base64(48)
|
|
75
|
+
|
|
76
|
+
new(
|
|
77
|
+
id: SecureRandom.urlsafe_base64(24),
|
|
78
|
+
redirect_uris: redirect_uris,
|
|
79
|
+
token_endpoint_auth_method: auth_method,
|
|
80
|
+
grant_types: grant_types,
|
|
81
|
+
response_types: response_types,
|
|
82
|
+
client_name: client_name,
|
|
83
|
+
client_uri: client_uri,
|
|
84
|
+
scope: metadata[:scope]&.to_s,
|
|
85
|
+
secret: secret,
|
|
86
|
+
secret_hash: secret && Digest::SHA384.hexdigest(secret),
|
|
87
|
+
client_id_issued_at: issued_at,
|
|
88
|
+
expiry: issued_at + lifetime,
|
|
89
|
+
registration_ip: registration_ip,
|
|
90
|
+
registration_remote_addr: registration_remote_addr,
|
|
91
|
+
registration_x_forwarded_for: registration_x_forwarded_for,
|
|
92
|
+
ignore_localhost_redirect_uri_port: ignore_localhost_redirect_uri_port,
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.validate_redirect_uris(given)
|
|
97
|
+
raise ValidationError.new(:invalid_redirect_uri, 'redirect_uris is required and must be a non-empty array') unless given.is_a?(Array) && !given.empty?
|
|
98
|
+
raise ValidationError.new(:invalid_redirect_uri, "redirect_uris must not exceed #{MAX_REDIRECT_URIS} entries") if given.size > MAX_REDIRECT_URIS
|
|
99
|
+
|
|
100
|
+
given.map do |uri|
|
|
101
|
+
str = uri.to_s
|
|
102
|
+
parsed = begin
|
|
103
|
+
Addressable::URI.parse(str)
|
|
104
|
+
rescue Addressable::URI::InvalidURIError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
raise ValidationError.new(:invalid_redirect_uri, "redirect_uri must not exceed #{MAX_URI_LENGTH} characters") if str.length > MAX_URI_LENGTH
|
|
108
|
+
raise ValidationError.new(:invalid_redirect_uri, "invalid redirect_uri: #{str}") unless parsed&.scheme
|
|
109
|
+
raise ValidationError.new(:invalid_redirect_uri, "redirect_uri must not contain a fragment: #{str}") if parsed.fragment
|
|
110
|
+
raise ValidationError.new(:invalid_redirect_uri, "redirect_uri scheme not allowed: #{str}") if DANGEROUS_REDIRECT_URI_SCHEMES.include?(parsed.scheme.downcase)
|
|
111
|
+
|
|
112
|
+
str
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.validate_client_uri(given)
|
|
117
|
+
return if given.nil?
|
|
118
|
+
|
|
119
|
+
str = given.to_s
|
|
120
|
+
raise ValidationError.new(:invalid_client_metadata, "client_uri must not exceed #{MAX_URI_LENGTH} characters") if str.length > MAX_URI_LENGTH
|
|
121
|
+
|
|
122
|
+
parsed = begin
|
|
123
|
+
Addressable::URI.parse(str)
|
|
124
|
+
rescue Addressable::URI::InvalidURIError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
raise ValidationError.new(:invalid_client_metadata, "invalid client_uri: #{str}") unless parsed&.scheme && parsed.host
|
|
128
|
+
|
|
129
|
+
str
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.from_json(hash)
|
|
133
|
+
attrs = hash.dup
|
|
134
|
+
attrs.delete(:ttl)
|
|
135
|
+
new(**attrs)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def initialize(id:, redirect_uris:, token_endpoint_auth_method:, grant_types:, response_types:, client_id_issued_at:, expiry:, secret: nil, secret_hash: nil, client_name: nil, client_uri: nil, scope: nil, preferred_key_group: nil, registration_ip: nil, registration_remote_addr: nil, registration_x_forwarded_for: nil, ignore_localhost_redirect_uri_port: true)
|
|
139
|
+
@id = id
|
|
140
|
+
@redirect_uris = redirect_uris
|
|
141
|
+
@token_endpoint_auth_method = token_endpoint_auth_method
|
|
142
|
+
@grant_types = grant_types
|
|
143
|
+
@response_types = response_types
|
|
144
|
+
@client_id_issued_at = client_id_issued_at
|
|
145
|
+
@expiry = expiry
|
|
146
|
+
@secret = secret
|
|
147
|
+
@secret_hash = secret_hash
|
|
148
|
+
@client_name = client_name
|
|
149
|
+
@client_uri = client_uri
|
|
150
|
+
@scope = scope
|
|
151
|
+
@preferred_key_group = preferred_key_group
|
|
152
|
+
@registration_ip = registration_ip
|
|
153
|
+
@registration_remote_addr = registration_remote_addr
|
|
154
|
+
@registration_x_forwarded_for = registration_x_forwarded_for
|
|
155
|
+
@ignore_localhost_redirect_uri_port = ignore_localhost_redirect_uri_port
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
attr_reader :id, :redirect_uris, :token_endpoint_auth_method, :grant_types, :response_types,
|
|
159
|
+
:client_id_issued_at, :expiry, :secret, :secret_hash, :client_name, :client_uri, :scope,
|
|
160
|
+
:preferred_key_group, :registration_ip, :registration_remote_addr, :registration_x_forwarded_for,
|
|
161
|
+
:ignore_localhost_redirect_uri_port
|
|
162
|
+
|
|
163
|
+
def confidential?
|
|
164
|
+
token_endpoint_auth_method != 'none'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Public clients have no secret to bind the authorization code, so PKCE is mandatory.
|
|
168
|
+
def require_pkce
|
|
169
|
+
!confidential?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def active?(now = Time.now)
|
|
173
|
+
expiry > now.to_i
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The client object the OIDC authorization/token endpoints consume. Dynamic records carry
|
|
177
|
+
# no name (so operator rules keyed on name never match them) and pass through the secret
|
|
178
|
+
# hash only for confidential clients. skip_consent and scopes default to the conservative
|
|
179
|
+
# values and are supplied by the provider from the DynamicClients middleware options.
|
|
180
|
+
def to_client_registration(skip_consent: false, scopes: ClientRegistration::IMPLICIT_SCOPES)
|
|
181
|
+
ClientRegistration.new(
|
|
182
|
+
id: id,
|
|
183
|
+
redirect_uris: redirect_uris,
|
|
184
|
+
secret_hash: confidential? ? secret_hash : nil,
|
|
185
|
+
preferred_key_group: preferred_key_group,
|
|
186
|
+
require_pkce: require_pkce,
|
|
187
|
+
confidential: confidential?,
|
|
188
|
+
ignore_localhost_redirect_uri_port: ignore_localhost_redirect_uri_port,
|
|
189
|
+
skip_consent: skip_consent,
|
|
190
|
+
scopes: scopes,
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def as_log
|
|
195
|
+
{
|
|
196
|
+
id: id,
|
|
197
|
+
token_endpoint_auth_method: token_endpoint_auth_method,
|
|
198
|
+
redirect_uris: redirect_uris,
|
|
199
|
+
grant_types: grant_types,
|
|
200
|
+
response_types: response_types,
|
|
201
|
+
client_name: client_name,
|
|
202
|
+
client_uri: client_uri,
|
|
203
|
+
scope: scope,
|
|
204
|
+
client_id_issued_at: client_id_issued_at,
|
|
205
|
+
expiry: expiry,
|
|
206
|
+
dynamic: true,
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def as_json
|
|
211
|
+
{
|
|
212
|
+
id: id,
|
|
213
|
+
secret_hash: secret_hash,
|
|
214
|
+
redirect_uris: redirect_uris,
|
|
215
|
+
grant_types: grant_types,
|
|
216
|
+
response_types: response_types,
|
|
217
|
+
token_endpoint_auth_method: token_endpoint_auth_method,
|
|
218
|
+
client_name: client_name,
|
|
219
|
+
client_uri: client_uri,
|
|
220
|
+
scope: scope,
|
|
221
|
+
preferred_key_group: preferred_key_group,
|
|
222
|
+
client_id_issued_at: client_id_issued_at,
|
|
223
|
+
expiry: expiry,
|
|
224
|
+
ttl: expiry,
|
|
225
|
+
registration_ip: registration_ip,
|
|
226
|
+
registration_remote_addr: registration_remote_addr,
|
|
227
|
+
registration_x_forwarded_for: registration_x_forwarded_for,
|
|
228
|
+
ignore_localhost_redirect_uri_port: ignore_localhost_redirect_uri_port,
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# RFC 7591 §3.2.1 client information response. Includes client_secret only when freshly
|
|
233
|
+
# generated (the plaintext is never persisted, so it is available only right after register).
|
|
234
|
+
def registration_response
|
|
235
|
+
response = {
|
|
236
|
+
client_id: id,
|
|
237
|
+
client_id_issued_at: client_id_issued_at,
|
|
238
|
+
redirect_uris: redirect_uris,
|
|
239
|
+
grant_types: grant_types,
|
|
240
|
+
response_types: response_types,
|
|
241
|
+
token_endpoint_auth_method: token_endpoint_auth_method,
|
|
242
|
+
client_name: client_name,
|
|
243
|
+
client_uri: client_uri,
|
|
244
|
+
scope: scope,
|
|
245
|
+
}.compact
|
|
246
|
+
|
|
247
|
+
if confidential? && secret
|
|
248
|
+
response[:client_secret] = secret
|
|
249
|
+
response[:client_secret_expires_at] = expiry
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
response
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
data/lib/himari/id_token.rb
CHANGED
|
@@ -1,60 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'rack/oauth2'
|
|
2
4
|
require 'openid_connect'
|
|
3
5
|
require 'base64'
|
|
4
6
|
require 'json/jwt'
|
|
5
7
|
|
|
8
|
+
require 'himari/jwt_token'
|
|
9
|
+
|
|
6
10
|
module Himari
|
|
7
|
-
class IdToken
|
|
11
|
+
class IdToken < JwtToken
|
|
8
12
|
# @param authz [Himari::AuthorizationCode]
|
|
9
13
|
def self.from_authz(authz, **kwargs)
|
|
10
|
-
|
|
11
14
|
new(
|
|
12
15
|
claims: authz.claims,
|
|
13
16
|
client_id: authz.client_id,
|
|
14
17
|
nonce: authz.nonce,
|
|
15
18
|
lifetime: authz.lifetime.is_a?(Integer) ? authz.lifetime : authz.lifetime.id_token, # compat
|
|
16
|
-
**kwargs
|
|
19
|
+
**kwargs,
|
|
17
20
|
)
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
def initialize(
|
|
21
|
-
|
|
22
|
-
@client_id = client_id
|
|
23
|
+
def initialize(nonce:, access_token: nil, **kwargs)
|
|
24
|
+
super(**kwargs)
|
|
23
25
|
@nonce = nonce
|
|
24
|
-
@signing_key = signing_key
|
|
25
|
-
@issuer = issuer
|
|
26
26
|
@access_token = access_token
|
|
27
|
-
@time = time
|
|
28
|
-
@lifetime = lifetime
|
|
29
27
|
end
|
|
30
28
|
|
|
31
|
-
attr_reader :
|
|
29
|
+
attr_reader :nonce
|
|
32
30
|
|
|
33
31
|
def final_claims
|
|
34
32
|
# https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
aud: @client_id,
|
|
38
|
-
iat: @time.to_i,
|
|
39
|
-
nbf: @time.to_i,
|
|
40
|
-
exp: (@time + @lifetime).to_i,
|
|
33
|
+
standard_claims.merge(
|
|
34
|
+
@nonce ? {nonce: @nonce} : {},
|
|
41
35
|
).merge(
|
|
42
|
-
@
|
|
43
|
-
).merge(
|
|
44
|
-
@access_token ? { at_hash: at_hash } : {}
|
|
36
|
+
@access_token ? {at_hash: at_hash} : {},
|
|
45
37
|
)
|
|
46
38
|
end
|
|
47
39
|
|
|
48
40
|
def at_hash
|
|
49
|
-
return
|
|
50
|
-
dgst = @signing_key.hash_function.digest(@access_token)
|
|
51
|
-
Base64.urlsafe_encode64(dgst[0, dgst.size/2], padding: false)
|
|
52
|
-
end
|
|
41
|
+
return unless @access_token
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
jwt.kid = @signing_key.id
|
|
57
|
-
jwt.sign(@signing_key.pkey, @signing_key.alg.to_sym).to_s
|
|
43
|
+
dgst = signing_key.hash_function.digest(@access_token)
|
|
44
|
+
Base64.urlsafe_encode64(dgst[0, dgst.size / 2], padding: false)
|
|
58
45
|
end
|
|
59
46
|
end
|
|
60
47
|
end
|
data/lib/himari/item_provider.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Himari
|
|
2
4
|
module ItemProvider
|
|
3
5
|
# :nocov:
|
|
4
6
|
# Return items searched by hints. This method can perform fuzzy match with hints. OTOH is not expected to return exact match results.
|
|
5
7
|
# Use Item#match_hint? to do exact match in later process. See also: ProviderChain
|
|
6
|
-
def collect(**hints)
|
|
8
|
+
def collect(**hints)
|
|
7
9
|
raise NotImplementedError
|
|
8
10
|
end
|
|
9
11
|
# :nocov:
|