Skip to content

Commit 12a770a

Browse files
Merge pull request #486 from cedarcode/temciuc--verify-top-origin
Verify `topOrigin` during credential registration and authentication
2 parents bc1d035 + 317228e commit 12a770a

File tree

10 files changed

+1296
-1
lines changed

10 files changed

+1296
-1
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- Add support for crossOrigin/topOrigin verification during credential registration and authentication. [#486](https://github.com/cedarcode/webauthn-ruby/pull/486) [@nicolastemciuc]
8+
59
## [v3.4.3] - 2025-10-23
610

711
### Fixed
@@ -494,3 +498,4 @@ Note: Both additions should help making it compatible with Chrome for Android 70
494498
[@jdongelmans]: https://github.com/jdongelmans
495499
[@petergoldstein]: https://github.com/petergoldstein
496500
[@ClearlyClaire]: https://github.com/ClearlyClaire
501+
[@nicolastemciuc]: https://github.com/nicolastemciuc

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ WebAuthn.configure do |config|
104104
# 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.
105105
config.allowed_origins = ["https://auth.example.com"]
106106

107+
# When operating within iframes or embedded contexts, you may need to restrict
108+
# which top-level origins are permitted to host WebAuthn ceremonies.
109+
#
110+
# crossOrigin / topOrigin verification is DISABLED by default:
111+
# config.verify_cross_origin = false
112+
#
113+
# When `verify_cross_origin` is false, any `crossOrigin` / `topOrigin` values reported by the browser
114+
# are ignored. As a result, credentials created or used within a cross-origin iframe will be treated
115+
# as valid.
116+
#
117+
# When `verify_cross_origin` is true, you can either:
118+
#
119+
# (A) Allow only specific top-level origins to embed your ceremony
120+
# (each entry must match the browser-reported `topOrigin` during registration/authentication):
121+
#
122+
# config.allowed_top_origins = ["https://app.example.com"]
123+
#
124+
# (B) Forbid ANY cross-origin iframe usage altogether
125+
# (this rejects creation/authentication whenever `crossOrigin` is true):
126+
#
127+
# config.allowed_top_origins = []
128+
#
129+
# Note: if `verify_cross_origin` is not enabled, any values set in `allowed_top_origins`
130+
# will be ignored.
131+
107132
# Relying Party name for display purposes
108133
config.rp_name = "Example Inc."
109134

lib/webauthn/authenticator_response.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class ChallengeVerificationError < VerificationError; end
1414
class OriginVerificationError < VerificationError; end
1515
class RpIdVerificationError < VerificationError; end
1616
class TokenBindingVerificationError < VerificationError; end
17+
class TopOriginVerificationError < VerificationError; end
1718
class TypeVerificationError < VerificationError; end
1819
class UserPresenceVerificationError < VerificationError; end
1920
class UserVerifiedVerificationError < VerificationError; end
@@ -33,6 +34,7 @@ def verify(expected_challenge, expected_origin = nil, user_presence: nil, user_v
3334
verify_item(:token_binding)
3435
verify_item(:challenge, expected_challenge)
3536
verify_item(:origin, expected_origin)
37+
verify_item(:top_origin) if needs_top_origin_verification?
3638
verify_item(:authenticator_data)
3739

3840
verify_item(
@@ -84,6 +86,12 @@ def valid_token_binding?
8486
client_data.valid_token_binding_format?
8587
end
8688

89+
def valid_top_origin?
90+
return false unless client_data.cross_origin
91+
92+
relying_party.allowed_top_origins&.include?(client_data.top_origin)
93+
end
94+
8795
def valid_challenge?(expected_challenge)
8896
OpenSSL.secure_compare(client_data.challenge, expected_challenge)
8997
end
@@ -121,5 +129,9 @@ def rp_id_from_origin(expected_origin)
121129
def type
122130
raise NotImplementedError, "Please define #type method in subclass"
123131
end
132+
133+
def needs_top_origin_verification?
134+
relying_party.verify_cross_origin && (client_data.cross_origin || client_data.top_origin)
135+
end
124136
end
125137
end

lib/webauthn/client_data.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ def token_binding
3131
data["tokenBinding"]
3232
end
3333

34+
def cross_origin
35+
data["crossOrigin"]
36+
end
37+
38+
def top_origin
39+
data["topOrigin"]
40+
end
41+
3442
def valid_token_binding_format?
3543
if token_binding
3644
token_binding.is_a?(Hash) && VALID_TOKEN_BINDING_STATUSES.include?(token_binding["status"])

lib/webauthn/configuration.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ class Configuration
2424
:origin=,
2525
:allowed_origins,
2626
:allowed_origins=,
27+
:allowed_top_origins,
28+
:allowed_top_origins=,
2729
:verify_attestation_statement,
2830
:verify_attestation_statement=,
31+
:verify_cross_origin,
32+
:verify_cross_origin=,
2933
:credential_options_timeout,
3034
:credential_options_timeout=,
3135
:silent_authentication,

lib/webauthn/fake_client.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ module WebAuthn
1010
class FakeClient
1111
TYPES = { create: "webauthn.create", get: "webauthn.get" }.freeze
1212

13-
attr_reader :origin, :token_binding, :encoding
13+
attr_reader :origin, :cross_origin, :top_origin, :token_binding, :encoding
1414

1515
def initialize(
1616
origin = fake_origin,
17+
cross_origin: nil,
18+
top_origin: nil,
1719
token_binding: nil,
1820
authenticator: WebAuthn::FakeAuthenticator.new,
1921
encoding: WebAuthn.configuration.encoding
2022
)
2123
@origin = origin
24+
@cross_origin = cross_origin
25+
@top_origin = top_origin
2226
@token_binding = token_binding
2327
@authenticator = authenticator
2428
@encoding = encoding
@@ -137,6 +141,14 @@ def data_json_for(method, challenge)
137141
data[:tokenBinding] = token_binding
138142
end
139143

144+
if cross_origin
145+
data[:crossOrigin] = cross_origin
146+
end
147+
148+
if top_origin
149+
data[:topOrigin] = top_origin
150+
end
151+
140152
data.to_json
141153
end
142154

lib/webauthn/relying_party.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ def initialize(
1919
algorithms: DEFAULT_ALGORITHMS.dup,
2020
encoding: WebAuthn::Encoder::STANDARD_ENCODING,
2121
allowed_origins: nil,
22+
allowed_top_origins: nil,
2223
origin: nil,
2324
id: nil,
2425
name: nil,
2526
verify_attestation_statement: true,
27+
verify_cross_origin: false,
2628
credential_options_timeout: 120000,
2729
silent_authentication: false,
2830
acceptable_attestation_types: ['None', 'Self', 'Basic', 'AttCA', 'Basic_or_AttCA', 'AnonCA'],
@@ -32,9 +34,11 @@ def initialize(
3234
@algorithms = algorithms
3335
@encoding = encoding
3436
@allowed_origins = allowed_origins
37+
@allowed_top_origins = allowed_top_origins
3538
@id = id
3639
@name = name
3740
@verify_attestation_statement = verify_attestation_statement
41+
@verify_cross_origin = verify_cross_origin
3842
@credential_options_timeout = credential_options_timeout
3943
@silent_authentication = silent_authentication
4044
@acceptable_attestation_types = acceptable_attestation_types
@@ -46,9 +50,11 @@ def initialize(
4650
attr_accessor :algorithms,
4751
:encoding,
4852
:allowed_origins,
53+
:allowed_top_origins,
4954
:id,
5055
:name,
5156
:verify_attestation_statement,
57+
:verify_cross_origin,
5258
:credential_options_timeout,
5359
:silent_authentication,
5460
:acceptable_attestation_types,

spec/spec_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def fake_origin
6363
"http://localhost"
6464
end
6565

66+
def fake_top_origin
67+
"http://localhost.org"
68+
end
69+
6670
def fake_challenge
6771
SecureRandom.random_bytes(32)
6872
end

0 commit comments

Comments
 (0)