Skip to content

Commit

Permalink
feat: allow multiple origins set per RelyingParty
Browse files Browse the repository at this point in the history
* add a possibility to set `allowed_origins` configuration option that would be an alternative to `origin`

* update Readme

* add deprecation warning

* adjust test suite

* overwrite writer to consistently trigger deprecation warnings

* fix origin extraction code
  • Loading branch information
obroshnij committed Feb 12, 2025
1 parent d02bd04 commit ba324c7
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 153 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ For a Rails application this would go in `config/initializers/webauthn.rb`.
WebAuthn.configure do |config|
# This value needs to match `window.location.origin` evaluated by
# the User Agent during registration and authentication ceremonies.
config.origin = "https://auth.example.com"
# Multiple origins can be used when needed. Using more than one will imply you MUST configure rp_id explicitely. If you need your credentials to be bound to a single origin but you have more than one tenant, please see [our Advanced Configuration section](https://github.com/cedarcode/webauthn-ruby/blob/master/docs/advanced_configuration.md) instead of adding multiple origins.
config.allowed_origins = [
"https://auth.example.com"
]

# Relying Party name for display purposes
config.rp_name = "Example Inc."
Expand Down
18 changes: 14 additions & 4 deletions lib/webauthn/authenticator_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,20 @@ def initialize(client_data_json:, relying_party: WebAuthn.configuration.relying_
end

def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_verification: nil, rp_id: nil)
expected_origin ||= relying_party.origin || raise("Unspecified expected origin")
expected_origin ||= relying_party.allowed_origins || raise("Unspecified expected origin")

rp_id ||= relying_party.id

verify_item(:type)
verify_item(:token_binding)
verify_item(:challenge, expected_challenge)
verify_item(:origin, expected_origin)
verify_item(:authenticator_data)
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))

verify_item(
:rp_id,
rp_id || rp_id_from_origin(expected_origin)
)

# Fallback to RP configuration unless user_presence is passed in explicitely
if user_presence.nil? && !relying_party.silent_authentication || user_presence
Expand Down Expand Up @@ -84,10 +89,14 @@ def valid_challenge?(expected_challenge)
end

def valid_origin?(expected_origin)
expected_origin && (client_data.origin == expected_origin)
return false unless expected_origin

expected_origin.include?(client_data.origin)
end

def valid_rp_id?(rp_id)
return false unless rp_id

OpenSSL::Digest::SHA256.digest(rp_id) == authenticator_data.rp_id_hash
end

Expand All @@ -105,8 +114,9 @@ def valid_user_verified?
authenticator_data.user_verified?
end

# Extract RP ID from origin in case rp_id is not provided explicitly
def rp_id_from_origin(expected_origin)
URI.parse(expected_origin).host
URI.parse(expected_origin.first).host if expected_origin.size == 1
end

def type
Expand Down
10 changes: 4 additions & 6 deletions lib/webauthn/client_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,10 @@ def hash

def data
@data ||=
begin
if client_data_json
JSON.parse(client_data_json)
else
raise ClientDataMissingError, "Client Data JSON is missing"
end
if client_data_json
JSON.parse(client_data_json)
else
raise ClientDataMissingError, "Client Data JSON is missing"
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/webauthn/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class Configuration
:encoding=,
:origin,
:origin=,
:allowed_origins,
:allowed_origins=,
:verify_attestation_statement,
:verify_attestation_statement=,
:credential_options_timeout,
Expand Down
25 changes: 20 additions & 5 deletions lib/webauthn/relying_party.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ module WebAuthn
class RootCertificateFinderNotSupportedError < Error; end

class RelyingParty
DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze

def self.if_pss_supported(algorithm)
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
end

DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze

def initialize(
algorithms: DEFAULT_ALGORITHMS.dup,
encoding: WebAuthn::Encoder::STANDARD_ENCODING,
allowed_origins: nil,
origin: nil,
id: nil,
name: nil,
Expand All @@ -30,20 +31,21 @@ def initialize(
)
@algorithms = algorithms
@encoding = encoding
@origin = origin
@allowed_origins = allowed_origins
@id = id
@name = name
@verify_attestation_statement = verify_attestation_statement
@credential_options_timeout = credential_options_timeout
@silent_authentication = silent_authentication
@acceptable_attestation_types = acceptable_attestation_types
@legacy_u2f_appid = legacy_u2f_appid
self.origin = origin
self.attestation_root_certificates_finders = attestation_root_certificates_finders
end

attr_accessor :algorithms,
:encoding,
:origin,
:allowed_origins,
:id,
:name,
:verify_attestation_statement,
Expand All @@ -52,7 +54,7 @@ def initialize(
:acceptable_attestation_types,
:legacy_u2f_appid

attr_reader :attestation_root_certificates_finders
attr_reader :attestation_root_certificates_finders, :origin

# This is the user-data encoder.
# Used to decode user input and to encode data provided to the user.
Expand Down Expand Up @@ -118,5 +120,18 @@ def verify_authentication(
block_given? ? [webauthn_credential, stored_credential] : webauthn_credential
end
end

# DEPRECATED: This method will be removed in future.
def origin=(new_origin)
return if new_origin.nil?

warn(
"DEPRECATION WARNING: `WebAuthn.origin` is deprecated and will be removed in future. "\
"Please use `WebAuthn.allowed_origins` instead "\
"that also allows configuring multiple origins per Relying Party"
)

@allowed_origins ||= Array(new_origin) # rubocop:disable Naming/MemoizedInstanceVariableName
end
end
end
Loading

0 comments on commit ba324c7

Please sign in to comment.