Class: StandardId::ClientApplication

Inherits:
ApplicationRecord show all
Defined in:
app/models/standard_id/client_application.rb

Class Method Summary collapse

Instance Method Summary collapse

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

Returns:

  • (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_arrayObject



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

Returns:

  • (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", **options)
  client_secret_credentials.create!({
    name: name,
    client_id: client_id,
    scopes: scopes
  }.merge(options))
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_arrayObject



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_secretObject

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

Returns:

  • (Boolean)


134
135
136
# File 'app/models/standard_id/client_application.rb', line 134

def public?
  client_type == "public"
end

#redirect_uris_arrayObject

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_arrayObject



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_arrayObject



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

Returns:

  • (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

Returns:

  • (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

Returns:

  • (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.

Returns:

  • (Boolean)


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