Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow multiple origins set per RelyingParty #431

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ 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
17 changes: 13 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 @@ -106,7 +115,7 @@ def valid_user_verified?
end

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
Loading