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 Nov 5, 2024
1 parent d02bd04 commit 3f85603
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 127 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@ WebAuthn.configure do |config|
# the User Agent during registration and authentication ceremonies.
config.origin = "https://auth.example.com"

# For the case when a relying party has multiple origins
# (e.g. different subdomains, native client sending android:apk-key-hash:... like origins in clientDataJson, etc...),
# you can set the `allowed_origins` instead of a single `origin` above
#
# config.allowed_origins = [
# "https://auth.example.com",
# "android:apk-key-hash:blablablablablalblalla"
# ]
#
# Note: in this case setting config.rp_id is mandatory

# Relying Party name for display purposes
config.rp_name = "Example Inc."

Expand Down
32 changes: 28 additions & 4 deletions lib/webauthn/authenticator_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,21 @@ 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 || [relying_party.origin] || 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))

# Note: we are trying to guess from 'expected_origin' only in case it's a single origin (array that contains a single element)
# rp_id should either be explicitly set or guessed from only a single 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 @@ -83,11 +89,21 @@ def valid_challenge?(expected_challenge)
OpenSSL.secure_compare(client_data.challenge, expected_challenge)
end

# @return [Boolean]
# @param [Array<String>] expected_origin
# Validate if one of the allowed origins configured for RP is matching the one received from client
def valid_origin?(expected_origin)
expected_origin && (client_data.origin == expected_origin)
return false unless expected_origin

expected_origin.include?(client_data.origin)
end

# @return [Boolean]
# @param [String] rp_id
# Validate if RP ID is matching the one received from client
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 +121,16 @@ def valid_user_verified?
authenticator_data.user_verified?
end

# @return [String, nil]
# @param [String, Array, nil] expected_origin
# 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
case expected_origin
when Array
URI.parse(expected_origin.first).host if expected_origin.size == 1
when String
URI.parse(expected_origin).host
end
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
22 changes: 19 additions & 3 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,19 +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,
:allowed_origins,
:origin,
:id,
:name,
Expand Down Expand Up @@ -118,5 +121,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)
unless 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"
)
end

@origin = new_origin
end
end
end
Loading

0 comments on commit 3f85603

Please sign in to comment.