Skip to content

Commit 4e1f5a7

Browse files
author
Vitalii Tereshchenko
committed
feat: allow multiple origins set per RelyingParty
* 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
1 parent d02bd04 commit 4e1f5a7

File tree

7 files changed

+330
-153
lines changed

7 files changed

+330
-153
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,12 @@ For a Rails application this would go in `config/initializers/webauthn.rb`.
104104
WebAuthn.configure do |config|
105105
# This value needs to match `window.location.origin` evaluated by
106106
# the User Agent during registration and authentication ceremonies.
107-
config.origin = "https://auth.example.com"
107+
# Multiple origins can be used in more complex scenarios (e.g. different subdomains, native client sending android:apk-key-hash:... etc.)
108+
109+
config.allowed_origins = [
110+
"https://auth.example.com",
111+
"android:apk-key-hash:blablablablablalblalla"
112+
]
108113

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

lib/webauthn/authenticator_response.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,23 @@ def initialize(client_data_json:, relying_party: WebAuthn.configuration.relying_
2525
end
2626

2727
def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_verification: nil, rp_id: nil)
28-
expected_origin ||= relying_party.origin || raise("Unspecified expected origin")
28+
expected_origin ||= relying_party.allowed_origins || raise("Unspecified expected origin")
29+
2930
rp_id ||= relying_party.id
3031

3132
verify_item(:type)
3233
verify_item(:token_binding)
3334
verify_item(:challenge, expected_challenge)
3435
verify_item(:origin, expected_origin)
3536
verify_item(:authenticator_data)
36-
verify_item(:rp_id, rp_id || rp_id_from_origin(expected_origin))
37+
38+
# NOTE: we are trying to guess from 'expected_origin' only in case it's a single origin
39+
# (array that contains a single element)
40+
# rp_id should either be explicitly set or guessed from only a single origin
41+
verify_item(
42+
:rp_id,
43+
rp_id || rp_id_from_origin(expected_origin)
44+
)
3745

3846
# Fallback to RP configuration unless user_presence is passed in explicitely
3947
if user_presence.nil? && !relying_party.silent_authentication || user_presence
@@ -84,10 +92,14 @@ def valid_challenge?(expected_challenge)
8492
end
8593

8694
def valid_origin?(expected_origin)
87-
expected_origin && (client_data.origin == expected_origin)
95+
return false unless expected_origin
96+
97+
expected_origin.include?(client_data.origin)
8898
end
8999

90100
def valid_rp_id?(rp_id)
101+
return false unless rp_id
102+
91103
OpenSSL::Digest::SHA256.digest(rp_id) == authenticator_data.rp_id_hash
92104
end
93105

@@ -105,8 +117,9 @@ def valid_user_verified?
105117
authenticator_data.user_verified?
106118
end
107119

120+
# Extract RP ID from origin in case rp_id is not provided explicitly
108121
def rp_id_from_origin(expected_origin)
109-
URI.parse(expected_origin).host
122+
URI.parse(expected_origin.first).host if expected_origin.size == 1
110123
end
111124

112125
def type

lib/webauthn/client_data.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,10 @@ def hash
4949

5050
def data
5151
@data ||=
52-
begin
53-
if client_data_json
54-
JSON.parse(client_data_json)
55-
else
56-
raise ClientDataMissingError, "Client Data JSON is missing"
57-
end
52+
if client_data_json
53+
JSON.parse(client_data_json)
54+
else
55+
raise ClientDataMissingError, "Client Data JSON is missing"
5856
end
5957
end
6058
end

lib/webauthn/configuration.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class Configuration
2222
:encoding=,
2323
:origin,
2424
:origin=,
25+
:allowed_origins,
26+
:allowed_origins=,
2527
:verify_attestation_statement,
2628
:verify_attestation_statement=,
2729
:credential_options_timeout,

lib/webauthn/relying_party.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ module WebAuthn
99
class RootCertificateFinderNotSupportedError < Error; end
1010

1111
class RelyingParty
12+
DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze
13+
1214
def self.if_pss_supported(algorithm)
1315
OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) ? algorithm : nil
1416
end
1517

16-
DEFAULT_ALGORITHMS = ["ES256", "PS256", "RS256"].compact.freeze
17-
1818
def initialize(
1919
algorithms: DEFAULT_ALGORITHMS.dup,
2020
encoding: WebAuthn::Encoder::STANDARD_ENCODING,
21+
allowed_origins: nil,
2122
origin: nil,
2223
id: nil,
2324
name: nil,
@@ -30,20 +31,21 @@ def initialize(
3031
)
3132
@algorithms = algorithms
3233
@encoding = encoding
33-
@origin = origin
34+
@allowed_origins = allowed_origins
3435
@id = id
3536
@name = name
3637
@verify_attestation_statement = verify_attestation_statement
3738
@credential_options_timeout = credential_options_timeout
3839
@silent_authentication = silent_authentication
3940
@acceptable_attestation_types = acceptable_attestation_types
4041
@legacy_u2f_appid = legacy_u2f_appid
42+
self.origin = origin
4143
self.attestation_root_certificates_finders = attestation_root_certificates_finders
4244
end
4345

4446
attr_accessor :algorithms,
4547
:encoding,
46-
:origin,
48+
:allowed_origins,
4749
:id,
4850
:name,
4951
:verify_attestation_statement,
@@ -52,7 +54,7 @@ def initialize(
5254
:acceptable_attestation_types,
5355
:legacy_u2f_appid
5456

55-
attr_reader :attestation_root_certificates_finders
57+
attr_reader :attestation_root_certificates_finders, :origin
5658

5759
# This is the user-data encoder.
5860
# Used to decode user input and to encode data provided to the user.
@@ -118,5 +120,18 @@ def verify_authentication(
118120
block_given? ? [webauthn_credential, stored_credential] : webauthn_credential
119121
end
120122
end
123+
124+
# DEPRECATED: This method will be removed in future.
125+
def origin=(new_origin)
126+
return if new_origin.nil?
127+
128+
warn(
129+
"DEPRECATION WARNING: `WebAuthn.origin` is deprecated and will be removed in future. "\
130+
"Please use `WebAuthn.allowed_origins` instead "\
131+
"that also allows configuring multiple origins per Relying Party"
132+
)
133+
134+
@allowed_origins ||= Array(new_origin) # rubocop:disable Naming/MemoizedInstanceVariableName
135+
end
121136
end
122137
end

0 commit comments

Comments
 (0)