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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
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
36 changes: 32 additions & 4 deletions lib/webauthn/authenticator_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,25 @@ 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 +93,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 +125,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
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)
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