Class: Himari::DynamicClientRegistration
- Inherits:
-
Object
- Object
- Himari::DynamicClientRegistration
- 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
-
#client_id_issued_at ⇒ Object
readonly
Returns the value of attribute client_id_issued_at.
-
#client_name ⇒ Object
readonly
Returns the value of attribute client_name.
-
#client_uri ⇒ Object
readonly
Returns the value of attribute client_uri.
-
#expiry ⇒ Object
readonly
Returns the value of attribute expiry.
-
#grant_types ⇒ Object
readonly
Returns the value of attribute grant_types.
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#ignore_localhost_redirect_uri_port ⇒ Object
readonly
Returns the value of attribute ignore_localhost_redirect_uri_port.
-
#preferred_key_group ⇒ Object
readonly
Returns the value of attribute preferred_key_group.
-
#redirect_uris ⇒ Object
readonly
Returns the value of attribute redirect_uris.
-
#registration_ip ⇒ Object
readonly
Returns the value of attribute registration_ip.
-
#registration_remote_addr ⇒ Object
readonly
Returns the value of attribute registration_remote_addr.
-
#registration_x_forwarded_for ⇒ Object
readonly
Returns the value of attribute registration_x_forwarded_for.
-
#response_types ⇒ Object
readonly
Returns the value of attribute response_types.
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
-
#secret ⇒ Object
readonly
Returns the value of attribute secret.
-
#secret_hash ⇒ Object
readonly
Returns the value of attribute secret_hash.
-
#token_endpoint_auth_method ⇒ Object
readonly
Returns the value of attribute token_endpoint_auth_method.
Class Method Summary collapse
- .from_json(hash) ⇒ Object
-
.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.
- .validate_client_uri(given) ⇒ Object
- .validate_redirect_uris(given) ⇒ Object
Instance Method Summary collapse
- #active?(now = Time.now) ⇒ Boolean
- #as_json ⇒ Object
- #as_log ⇒ Object
- #confidential? ⇒ Boolean
-
#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
constructor
A new instance of DynamicClientRegistration.
-
#registration_response ⇒ Object
RFC 7591 §3.2.1 client information response.
-
#require_pkce ⇒ Object
Public clients have no secret to bind the authorization code, so PKCE is mandatory.
-
#to_client_registration(skip_consent: false, scopes: ClientRegistration::IMPLICIT_SCOPES) ⇒ Object
The client object the OIDC authorization/token endpoints consume.
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_at ⇒ Object (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_name ⇒ Object (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_uri ⇒ Object (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 |
#expiry ⇒ Object (readonly)
Returns the value of attribute expiry.
158 159 160 |
# File 'lib/himari/dynamic_client_registration.rb', line 158 def expiry @expiry end |
#grant_types ⇒ Object (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 |
#id ⇒ Object (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_port ⇒ Object (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_group ⇒ Object (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_uris ⇒ Object (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_ip ⇒ Object (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_addr ⇒ Object (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_for ⇒ Object (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_types ⇒ Object (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 |
#scope ⇒ Object (readonly)
Returns the value of attribute scope.
158 159 160 |
# File 'lib/himari/dynamic_client_registration.rb', line 158 def scope @scope end |
#secret ⇒ Object (readonly)
Returns the value of attribute secret.
158 159 160 |
# File 'lib/himari/dynamic_client_registration.rb', line 158 def secret @secret end |
#secret_hash ⇒ Object (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_method ⇒ Object (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.
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
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
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
172 173 174 |
# File 'lib/himari/dynamic_client_registration.rb', line 172 def active?(now = Time.now) expiry > now.to_i end |
#as_json ⇒ Object
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_log ⇒ Object
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
163 164 165 |
# File 'lib/himari/dynamic_client_registration.rb', line 163 def confidential? token_endpoint_auth_method != 'none' end |
#registration_response ⇒ Object
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_pkce ⇒ Object
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: , scopes: scopes, ) end |