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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/lib/himari/access_token.rb +77 -4
  4. data/lib/himari/access_token_jwt.rb +46 -0
  5. data/lib/himari/app.rb +101 -28
  6. data/lib/himari/authorization_code.rb +18 -4
  7. data/lib/himari/client_registration.rb +71 -4
  8. data/lib/himari/config.rb +8 -3
  9. data/lib/himari/decisions/authentication.rb +18 -2
  10. data/lib/himari/decisions/authorization.rb +18 -7
  11. data/lib/himari/decisions/base.rb +7 -3
  12. data/lib/himari/decisions/claims.rb +14 -9
  13. data/lib/himari/dynamic_client_registration.rb +255 -0
  14. data/lib/himari/id_token.rb +15 -28
  15. data/lib/himari/item_provider.rb +3 -1
  16. data/lib/himari/item_providers/oauth_client_metadata.rb +222 -0
  17. data/lib/himari/item_providers/static.rb +2 -0
  18. data/lib/himari/item_providers/storage.rb +33 -0
  19. data/lib/himari/jwt_token.rb +50 -0
  20. data/lib/himari/lifetime_value.rb +5 -3
  21. data/lib/himari/log_line.rb +2 -0
  22. data/lib/himari/middlewares/authentication_rule.rb +2 -0
  23. data/lib/himari/middlewares/authorization_rule.rb +2 -0
  24. data/lib/himari/middlewares/claims_rule.rb +2 -0
  25. data/lib/himari/middlewares/client.rb +2 -0
  26. data/lib/himari/middlewares/config.rb +2 -0
  27. data/lib/himari/middlewares/dynamic_clients.rb +55 -0
  28. data/lib/himari/middlewares/metadata_clients.rb +121 -0
  29. data/lib/himari/middlewares/signing_key.rb +2 -0
  30. data/lib/himari/provider_chain.rb +3 -1
  31. data/lib/himari/refresh_token.rb +93 -0
  32. data/lib/himari/rule.rb +2 -0
  33. data/lib/himari/rule_processor.rb +3 -0
  34. data/lib/himari/services/client_registration_endpoint.rb +78 -0
  35. data/lib/himari/services/downstream_authorization.rb +22 -7
  36. data/lib/himari/services/jwks_endpoint.rb +3 -1
  37. data/lib/himari/services/oidc_authorization_endpoint.rb +56 -3
  38. data/lib/himari/services/oidc_provider_metadata_endpoint.rb +30 -7
  39. data/lib/himari/services/oidc_token_endpoint.rb +225 -38
  40. data/lib/himari/services/oidc_userinfo_endpoint.rb +14 -8
  41. data/lib/himari/services/upstream_authentication.rb +62 -14
  42. data/lib/himari/session_data.rb +31 -2
  43. data/lib/himari/signing_key.rb +17 -14
  44. data/lib/himari/storages/base.rb +45 -1
  45. data/lib/himari/storages/filesystem.rb +14 -3
  46. data/lib/himari/storages/memory.rb +10 -2
  47. data/lib/himari/token_string.rb +40 -4
  48. data/lib/himari/version.rb +1 -1
  49. data/public/public/index.css +18 -0
  50. data/views/consent.erb +59 -0
  51. 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, progname, msg|
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
- @lifetime = x
46
+ x
37
47
  else
38
- @lifetime = LifetimeValue.from_integer(x)
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
- def self.valid_effects
12
- @valid_effects
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 provider; auth[:provider]; end
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
- raise UninitializedError, "Claims uninitialized; use decision.initialize_claims! to declare claims first (or rule order might be unintentional)" unless @claims
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
@@ -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(claims:, client_id:, nonce:, signing_key:, issuer:, access_token: nil, time: Time.now, lifetime: 3600)
21
- @claims = claims
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 :claims, :nonce, :signing_key
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
- claims.merge(
36
- iss: @issuer,
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
- @nonce ? { nonce: @nonce } : {}
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 nil unless @access_token
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
- def to_jwt
55
- jwt = JSON::JWT.new(final_claims)
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
@@ -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: