Class: StandardId::ClientApplication
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- StandardId::ClientApplication
- Defined in:
- app/models/standard_id/client_application.rb
Class Method Summary collapse
-
.parse_redirect_uri(value) ⇒ Object
Parse a redirect URI string into a URI object suitable for comparison.
Instance Method Summary collapse
- #activate! ⇒ Object
- #active? ⇒ Boolean
-
#authenticate_client_secret(secret) ⇒ Object
Check if client can authenticate with given secret.
- #code_challenge_methods_array ⇒ Object
- #confidential? ⇒ Boolean
-
#create_client_secret!(name: "Default Secret", **options) ⇒ Object
Generate a new client secret credential.
- #deactivate! ⇒ Object
- #grant_types_array ⇒ Object
-
#primary_client_secret ⇒ Object
Get the primary (first active) client secret.
- #public? ⇒ Boolean
-
#redirect_uris_array ⇒ Object
OAuth configuration helpers.
- #response_types_array ⇒ Object
-
#rotate_client_secret!(new_secret_name: "Rotated Secret #{Time.current.strftime('%Y%m%d')}", client_secret: SecureRandom.hex(32)) ⇒ Object
Client secret rotation support.
- #scopes_array ⇒ Object
- #supports_grant_type?(grant_type) ⇒ Boolean
- #supports_pkce_method?(method) ⇒ Boolean
- #supports_response_type?(response_type) ⇒ Boolean
-
#valid_redirect_uri?(uri) ⇒ Boolean
Validates a redirect_uri presented in an OAuth request against this client’s registered URIs.
Class Method Details
.parse_redirect_uri(value) ⇒ Object
Parse a redirect URI string into a URI object suitable for comparison. Returns nil for unparseable, relative, or scheme-less URIs.
119 120 121 122 123 124 125 126 127 128 |
# File 'app/models/standard_id/client_application.rb', line 119 def self.parse_redirect_uri(value) return nil if value.to_s.strip.empty? parsed = URI.parse(value.to_s.strip) return nil if parsed.scheme.blank? || parsed.host.blank? parsed rescue URI::InvalidURIError nil end |
Instance Method Details
#activate! ⇒ Object
45 46 47 |
# File 'app/models/standard_id/client_application.rb', line 45 def activate! update!(active: true, deactivated_at: nil) end |
#active? ⇒ Boolean
49 50 51 |
# File 'app/models/standard_id/client_application.rb', line 49 def active? active && deactivated_at.nil? end |
#authenticate_client_secret(secret) ⇒ Object
Check if client can authenticate with given secret
169 170 171 |
# File 'app/models/standard_id/client_application.rb', line 169 def authenticate_client_secret(secret) client_secret_credentials.active.find { |cred| cred.authenticate_client_secret(secret) } end |
#code_challenge_methods_array ⇒ Object
70 71 72 |
# File 'app/models/standard_id/client_application.rb', line 70 def code_challenge_methods_array code_challenge_methods.to_s.split(/\s+/).map(&:strip).reject(&:blank?) end |
#confidential? ⇒ Boolean
130 131 132 |
# File 'app/models/standard_id/client_application.rb', line 130 def confidential? client_type == "confidential" end |
#create_client_secret!(name: "Default Secret", **options) ⇒ Object
Generate a new client secret credential
139 140 141 142 143 144 145 |
# File 'app/models/standard_id/client_application.rb', line 139 def create_client_secret!(name: "Default Secret", **) client_secret_credentials.create!({ name: name, client_id: client_id, scopes: scopes }.merge()) end |
#deactivate! ⇒ Object
41 42 43 |
# File 'app/models/standard_id/client_application.rb', line 41 def deactivate! update!(active: false, deactivated_at: Time.current) end |
#grant_types_array ⇒ Object
62 63 64 |
# File 'app/models/standard_id/client_application.rb', line 62 def grant_types_array grant_types.to_s.split(/\s+/).map(&:strip).reject(&:blank?) end |
#primary_client_secret ⇒ Object
Get the primary (first active) client secret
148 149 150 |
# File 'app/models/standard_id/client_application.rb', line 148 def primary_client_secret client_secret_credentials.active.first end |
#public? ⇒ Boolean
134 135 136 |
# File 'app/models/standard_id/client_application.rb', line 134 def public? client_type == "public" end |
#redirect_uris_array ⇒ Object
OAuth configuration helpers
54 55 56 |
# File 'app/models/standard_id/client_application.rb', line 54 def redirect_uris_array redirect_uris.to_s.split(/\s+/).map(&:strip).reject(&:blank?) end |
#response_types_array ⇒ Object
66 67 68 |
# File 'app/models/standard_id/client_application.rb', line 66 def response_types_array response_types.to_s.split(/\s+/).map(&:strip).reject(&:blank?) end |
#rotate_client_secret!(new_secret_name: "Rotated Secret #{Time.current.strftime('%Y%m%d')}", client_secret: SecureRandom.hex(32)) ⇒ Object
Client secret rotation support
153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'app/models/standard_id/client_application.rb', line 153 def rotate_client_secret!(new_secret_name: "Rotated Secret #{Time.current.strftime('%Y%m%d')}", client_secret: SecureRandom.hex(32)) transaction do # Create new secret new_secret = create_client_secret!(name: new_secret_name, client_secret: client_secret) # Deactivate old secrets (but don't delete for audit trail) client_secret_credentials.where.not(id: new_secret.id).update_all( active: false, revoked_at: Time.current ) new_secret end end |
#scopes_array ⇒ Object
58 59 60 |
# File 'app/models/standard_id/client_application.rb', line 58 def scopes_array scopes.to_s.split(/\s+/).map(&:strip).reject(&:blank?) end |
#supports_grant_type?(grant_type) ⇒ Boolean
74 75 76 |
# File 'app/models/standard_id/client_application.rb', line 74 def supports_grant_type?(grant_type) grant_types_array.include?(grant_type.to_s) end |
#supports_pkce_method?(method) ⇒ Boolean
82 83 84 85 86 |
# File 'app/models/standard_id/client_application.rb', line 82 def supports_pkce_method?(method) return false unless require_pkce? normalized = method.to_s.downcase code_challenge_methods_array.any? { |m| m.downcase == normalized } end |
#supports_response_type?(response_type) ⇒ Boolean
78 79 80 |
# File 'app/models/standard_id/client_application.rb', line 78 def supports_response_type?(response_type) response_types_array.include?(response_type.to_s) end |
#valid_redirect_uri?(uri) ⇒ Boolean
Validates a redirect_uri presented in an OAuth request against this client’s registered URIs.
OAuth 2.0 (RFC 6749 §3.1.2) requires the authorization server to compare the registered redirect URI and the request redirect URI using simple string comparison, with the exception that the authorization server may redirect with additional query parameters. We implement a stricter scheme+host+port+path match: the request URI may add query or fragment segments, but the scheme, host, port, and path must exactly match a registered URI. This prevents a class of “query-string piggyback” attacks where a registered callback at /cb is abused with a crafted query string (or, worse, a different path segment like /cb/evil).
Subdomain wildcards are NOT supported — host must match exactly.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'app/models/standard_id/client_application.rb', line 102 def valid_redirect_uri?(uri) requested = self.class.parse_redirect_uri(uri) return false unless requested redirect_uris_array.any? do |registered_uri| registered = self.class.parse_redirect_uri(registered_uri) next false unless registered registered.scheme == requested.scheme && registered.host == requested.host && registered.port == requested.port && registered.path == requested.path end end |