Class: Himari::DynamicClientRegistration

Inherits:
Object
  • Object
show all
Defined in:
lib/himari/dynamic_client_registration.rb

Overview

A client created at runtime via RFC 7591 Dynamic Client Registration, persisted in storage. This is purely a storage/registration record: the registration endpoint interacts with it directly, while the OIDC endpoints only ever see the plain ClientRegistration produced by #to_client_registration at the provider layer.

Defined Under Namespace

Classes: ValidationError

Constant Summary collapse

REGISTRATION_LIFETIME =

Default registration lifetime; overridable per deployment via Middlewares::DynamicClients.

180 * 86400
SUPPORTED_GRANT_TYPES =
%w(authorization_code refresh_token).freeze
SUPPORTED_RESPONSE_TYPES =
%w(code).freeze
SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS =
%w(none client_secret_basic client_secret_post).freeze
DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD =
'client_secret_basic'
MAX_REDIRECT_URIS =
32
MAX_URI_LENGTH =
2000
MAX_CLIENT_NAME_LENGTH =
60
DANGEROUS_REDIRECT_URI_SCHEMES =
%w(javascript data vbscript file blob).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#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) ⇒ DynamicClientRegistration

Returns a new instance of DynamicClientRegistration.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/himari/dynamic_client_registration.rb', line 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)
  @id = id
  @redirect_uris = redirect_uris
  @token_endpoint_auth_method = token_endpoint_auth_method
  @grant_types = grant_types
  @response_types = response_types
  @client_id_issued_at = client_id_issued_at
  @expiry = expiry
  @secret = secret
  @secret_hash = secret_hash
  @client_name = client_name
  @client_uri = client_uri
  @scope = scope
  @preferred_key_group = preferred_key_group
  @registration_ip = registration_ip
  @registration_remote_addr = registration_remote_addr
  @registration_x_forwarded_for = registration_x_forwarded_for
  @ignore_localhost_redirect_uri_port = ignore_localhost_redirect_uri_port
end

Instance Attribute Details

#client_id_issued_atObject (readonly)

Returns the value of attribute client_id_issued_at.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def client_id_issued_at
  @client_id_issued_at
end

#client_nameObject (readonly)

Returns the value of attribute client_name.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def client_name
  @client_name
end

#client_uriObject (readonly)

Returns the value of attribute client_uri.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def client_uri
  @client_uri
end

#expiryObject (readonly)

Returns the value of attribute expiry.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def expiry
  @expiry
end

#grant_typesObject (readonly)

Returns the value of attribute grant_types.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def grant_types
  @grant_types
end

#idObject (readonly)

Returns the value of attribute id.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def id
  @id
end

#ignore_localhost_redirect_uri_portObject (readonly)

Returns the value of attribute ignore_localhost_redirect_uri_port.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def ignore_localhost_redirect_uri_port
  @ignore_localhost_redirect_uri_port
end

#preferred_key_groupObject (readonly)

Returns the value of attribute preferred_key_group.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def preferred_key_group
  @preferred_key_group
end

#redirect_urisObject (readonly)

Returns the value of attribute redirect_uris.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def redirect_uris
  @redirect_uris
end

#registration_ipObject (readonly)

Returns the value of attribute registration_ip.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def registration_ip
  @registration_ip
end

#registration_remote_addrObject (readonly)

Returns the value of attribute registration_remote_addr.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def registration_remote_addr
  @registration_remote_addr
end

#registration_x_forwarded_forObject (readonly)

Returns the value of attribute registration_x_forwarded_for.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def registration_x_forwarded_for
  @registration_x_forwarded_for
end

#response_typesObject (readonly)

Returns the value of attribute response_types.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def response_types
  @response_types
end

#scopeObject (readonly)

Returns the value of attribute scope.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def scope
  @scope
end

#secretObject (readonly)

Returns the value of attribute secret.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def secret
  @secret
end

#secret_hashObject (readonly)

Returns the value of attribute secret_hash.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def secret_hash
  @secret_hash
end

#token_endpoint_auth_methodObject (readonly)

Returns the value of attribute token_endpoint_auth_method.



158
159
160
# File 'lib/himari/dynamic_client_registration.rb', line 158

def token_endpoint_auth_method
  @token_endpoint_auth_method
end

Class Method Details

.from_json(hash) ⇒ Object



132
133
134
135
136
# File 'lib/himari/dynamic_client_registration.rb', line 132

def self.from_json(hash)
  attrs = hash.dup
  attrs.delete(:ttl)
  new(**attrs)
end

.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) ⇒ DynamicClientRegistration

Build and validate a registration from RFC 7591 client metadata.

Parameters:

  • metadata (Hash)

    parsed client metadata (symbolized keys) from the request body

Returns:

Raises:



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/himari/dynamic_client_registration.rb', line 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)
  raise ValidationError.new(:invalid_client_metadata, 'request body must be a JSON object') unless .is_a?(Hash)

  auth_method = .fetch(:token_endpoint_auth_method, DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD).to_s
  unless SUPPORTED_TOKEN_ENDPOINT_AUTH_METHODS.include?(auth_method)
    raise ValidationError.new(:invalid_client_metadata, "unsupported token_endpoint_auth_method: #{auth_method}")
  end

  grant_types = Array([:grant_types] || %w(authorization_code)).map(&:to_s)
  unless (grant_types - SUPPORTED_GRANT_TYPES).empty?
    raise ValidationError.new(:invalid_client_metadata, "unsupported grant_types: #{(grant_types - SUPPORTED_GRANT_TYPES).join(",")}")
  end

  response_types = Array([:response_types] || %w(code)).map(&:to_s)
  unless (response_types - SUPPORTED_RESPONSE_TYPES).empty?
    raise ValidationError.new(:invalid_client_metadata, "unsupported response_types: #{(response_types - SUPPORTED_RESPONSE_TYPES).join(",")}")
  end

  if response_types.include?('code') && !grant_types.include?('authorization_code')
    raise ValidationError.new(:invalid_client_metadata, 'response_type "code" requires grant_type "authorization_code"')
  end

  redirect_uris = validate_redirect_uris([:redirect_uris])

  client_name = [:client_name]&.to_s
  if client_name && client_name.length > MAX_CLIENT_NAME_LENGTH
    raise ValidationError.new(:invalid_client_metadata, "client_name must not exceed #{MAX_CLIENT_NAME_LENGTH} characters")
  end

  client_uri = validate_client_uri([:client_uri])

  issued_at = now.to_i
  secret = auth_method == 'none' ? nil : SecureRandom.urlsafe_base64(48)

  new(
    id: SecureRandom.urlsafe_base64(24),
    redirect_uris: redirect_uris,
    token_endpoint_auth_method: auth_method,
    grant_types: grant_types,
    response_types: response_types,
    client_name: client_name,
    client_uri: client_uri,
    scope: [:scope]&.to_s,
    secret: secret,
    secret_hash: secret && Digest::SHA384.hexdigest(secret),
    client_id_issued_at: issued_at,
    expiry: issued_at + lifetime,
    registration_ip: registration_ip,
    registration_remote_addr: registration_remote_addr,
    registration_x_forwarded_for: registration_x_forwarded_for,
    ignore_localhost_redirect_uri_port: ignore_localhost_redirect_uri_port,
  )
end

.validate_client_uri(given) ⇒ Object

Raises:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/himari/dynamic_client_registration.rb', line 116

def self.validate_client_uri(given)
  return if given.nil?

  str = given.to_s
  raise ValidationError.new(:invalid_client_metadata, "client_uri must not exceed #{MAX_URI_LENGTH} characters") if str.length > MAX_URI_LENGTH

  parsed = begin
    Addressable::URI.parse(str)
  rescue Addressable::URI::InvalidURIError
    nil
  end
  raise ValidationError.new(:invalid_client_metadata, "invalid client_uri: #{str}") unless parsed&.scheme && parsed.host

  str
end

.validate_redirect_uris(given) ⇒ Object

Raises:



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/himari/dynamic_client_registration.rb', line 96

def self.validate_redirect_uris(given)
  raise ValidationError.new(:invalid_redirect_uri, 'redirect_uris is required and must be a non-empty array') unless given.is_a?(Array) && !given.empty?
  raise ValidationError.new(:invalid_redirect_uri, "redirect_uris must not exceed #{MAX_REDIRECT_URIS} entries") if given.size > MAX_REDIRECT_URIS

  given.map do |uri|
    str = uri.to_s
    parsed = begin
      Addressable::URI.parse(str)
    rescue Addressable::URI::InvalidURIError
      nil
    end
    raise ValidationError.new(:invalid_redirect_uri, "redirect_uri must not exceed #{MAX_URI_LENGTH} characters") if str.length > MAX_URI_LENGTH
    raise ValidationError.new(:invalid_redirect_uri, "invalid redirect_uri: #{str}") unless parsed&.scheme
    raise ValidationError.new(:invalid_redirect_uri, "redirect_uri must not contain a fragment: #{str}") if parsed.fragment
    raise ValidationError.new(:invalid_redirect_uri, "redirect_uri scheme not allowed: #{str}") if DANGEROUS_REDIRECT_URI_SCHEMES.include?(parsed.scheme.downcase)

    str
  end
end

Instance Method Details

#active?(now = Time.now) ⇒ Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/himari/dynamic_client_registration.rb', line 172

def active?(now = Time.now)
  expiry > now.to_i
end

#as_jsonObject



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/himari/dynamic_client_registration.rb', line 210

def as_json
  {
    id: id,
    secret_hash: secret_hash,
    redirect_uris: redirect_uris,
    grant_types: grant_types,
    response_types: response_types,
    token_endpoint_auth_method: token_endpoint_auth_method,
    client_name: client_name,
    client_uri: client_uri,
    scope: scope,
    preferred_key_group: preferred_key_group,
    client_id_issued_at: client_id_issued_at,
    expiry: expiry,
    ttl: expiry,
    registration_ip: registration_ip,
    registration_remote_addr: registration_remote_addr,
    registration_x_forwarded_for: registration_x_forwarded_for,
    ignore_localhost_redirect_uri_port: ignore_localhost_redirect_uri_port,
  }
end

#as_logObject



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/himari/dynamic_client_registration.rb', line 194

def as_log
  {
    id: id,
    token_endpoint_auth_method: token_endpoint_auth_method,
    redirect_uris: redirect_uris,
    grant_types: grant_types,
    response_types: response_types,
    client_name: client_name,
    client_uri: client_uri,
    scope: scope,
    client_id_issued_at: client_id_issued_at,
    expiry: expiry,
    dynamic: true,
  }
end

#confidential?Boolean

Returns:

  • (Boolean)


163
164
165
# File 'lib/himari/dynamic_client_registration.rb', line 163

def confidential?
  token_endpoint_auth_method != 'none'
end

#registration_responseObject

RFC 7591 §3.2.1 client information response. Includes client_secret only when freshly generated (the plaintext is never persisted, so it is available only right after register).



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/himari/dynamic_client_registration.rb', line 234

def registration_response
  response = {
    client_id: id,
    client_id_issued_at: client_id_issued_at,
    redirect_uris: redirect_uris,
    grant_types: grant_types,
    response_types: response_types,
    token_endpoint_auth_method: token_endpoint_auth_method,
    client_name: client_name,
    client_uri: client_uri,
    scope: scope,
  }.compact

  if confidential? && secret
    response[:client_secret] = secret
    response[:client_secret_expires_at] = expiry
  end

  response
end

#require_pkceObject

Public clients have no secret to bind the authorization code, so PKCE is mandatory.



168
169
170
# File 'lib/himari/dynamic_client_registration.rb', line 168

def require_pkce
  !confidential?
end

#to_client_registration(skip_consent: false, scopes: ClientRegistration::IMPLICIT_SCOPES) ⇒ Object

The client object the OIDC authorization/token endpoints consume. Dynamic records carry no name (so operator rules keyed on name never match them) and pass through the secret hash only for confidential clients. skip_consent and scopes default to the conservative values and are supplied by the provider from the DynamicClients middleware options.



180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/himari/dynamic_client_registration.rb', line 180

def to_client_registration(skip_consent: false, scopes: ClientRegistration::IMPLICIT_SCOPES)
  ClientRegistration.new(
    id: id,
    redirect_uris: redirect_uris,
    secret_hash: confidential? ? secret_hash : nil,
    preferred_key_group: preferred_key_group,
    require_pkce: require_pkce,
    confidential: confidential?,
    ignore_localhost_redirect_uri_port: ignore_localhost_redirect_uri_port,
    skip_consent: skip_consent,
    scopes: scopes,
  )
end