Skip to content

Commit ebb45e2

Browse files
fix(webauthn): use session relying party id (#747)
This fixes an issue where an overridden Relying Party ID set in the session was not honooured.
1 parent b39c822 commit ebb45e2

6 files changed

Lines changed: 116 additions & 4 deletions

File tree

webauthn/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe
364364
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired
365365
shouldVerifyUserPresence := true
366366

367-
rpID := webauthn.Config.RPID
367+
rpID := session.GetRelyingPartyID(webauthn.Config.RPID)
368368
rpOrigins := webauthn.Config.RPOrigins
369369
rpTopOrigins := webauthn.Config.RPTopOrigins
370370

webauthn/login_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,44 @@ func TestValidateLoginLatchesUserVerified(t *testing.T) {
13931393
assert.True(t, credential.Flags.BackupState)
13941394
}
13951395

1396+
func TestValidateLoginUsesSessionRelyingPartyID(t *testing.T) {
1397+
parsedResponse, credPubKey, challenge, credentialID := testLoginSpecVectorNoneES256(t)
1398+
1399+
webauthn := &WebAuthn{
1400+
Config: &Config{
1401+
RPID: "example.com",
1402+
RPOrigins: []string{"https://example.org"},
1403+
},
1404+
}
1405+
1406+
userID := []byte(testUserID)
1407+
1408+
user := &defaultUser{
1409+
id: userID,
1410+
credentials: []Credential{
1411+
{
1412+
ID: credentialID,
1413+
PublicKey: credPubKey,
1414+
Flags: CredentialFlags{
1415+
UserPresent: true,
1416+
BackupEligible: true,
1417+
},
1418+
},
1419+
},
1420+
}
1421+
1422+
session := SessionData{
1423+
UserID: userID,
1424+
Challenge: challenge,
1425+
RelyingPartyID: "example.org",
1426+
}
1427+
1428+
credential, err := webauthn.ValidateLogin(user, session, parsedResponse)
1429+
require.NoError(t, err)
1430+
require.NotNil(t, credential)
1431+
assert.Equal(t, credentialID, credential.ID)
1432+
}
1433+
13961434
func TestValidatePasskeyLogin_Full(t *testing.T) {
13971435
parsedResponse, credPubKey, challenge, credentialID := testLoginSpecVectorNoneES256(t)
13981436

webauthn/registration.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse
164164

165165
var clientDataHash []byte
166166

167-
if clientDataHash, err = parsedResponse.Verify(session.Challenge, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.RPAllowCrossOrigin, shouldVerifyUser, shouldVerifyUserPresence, webauthn.Config.MDS, session.CredParams, webauthn.Config.Attestation, webauthn.Config.Signature); err != nil {
167+
if clientDataHash, err = parsedResponse.Verify(session.Challenge, session.GetRelyingPartyID(webauthn.Config.RPID), webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.RPAllowCrossOrigin, shouldVerifyUser, shouldVerifyUserPresence, webauthn.Config.MDS, session.CredParams, webauthn.Config.Attestation, webauthn.Config.Signature); err != nil {
168168
return nil, err
169169
}
170170

webauthn/registration_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,8 +790,34 @@ func TestCreateCredential_Full(t *testing.T) {
790790
}
791791
}
792792

793-
// TestCreateCredential_RejectsBackupStateWithoutBackupEligibility covers §7.1 step 18, which requires the BS bit to
794-
// be unset when the BE bit is unset. The equivalent assertion step is covered by the login validation.
793+
func TestCreateCredentialUsesSessionRelyingPartyID(t *testing.T) {
794+
body, challenge, credentialID := testRegistrationSpecVectorNoneES256(t)
795+
796+
parsedResponse, err := protocol.ParseCredentialCreationResponseBytes(body)
797+
require.NoError(t, err)
798+
799+
userID := []byte(testUserID)
800+
801+
w := &WebAuthn{
802+
Config: &Config{
803+
RPID: "example.com",
804+
RPOrigins: []string{"https://example.org"},
805+
},
806+
}
807+
808+
session := SessionData{
809+
Challenge: challenge,
810+
RelyingPartyID: "example.org",
811+
UserID: userID,
812+
CredParams: []protocol.CredentialParameter{{Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
813+
}
814+
815+
credential, err := w.CreateCredential(&defaultUser{id: userID}, session, parsedResponse)
816+
require.NoError(t, err)
817+
require.NotNil(t, credential)
818+
assert.Equal(t, credentialID, credential.ID)
819+
}
820+
795821
func TestCreateCredential_RejectsBackupStateWithoutBackupEligibility(t *testing.T) {
796822
body, challenge, _ := testRegistrationSpecVectorNoneES256Flags(t, protocol.FlagUserPresent|protocol.FlagBackupState|protocol.FlagAttestedCredentialData)
797823

webauthn/types_session.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,18 @@ type SessionData struct {
7575
CredParams []protocol.CredentialParameter `json:"credParams,omitempty" msg:"params,omitempty"`
7676
Mediation protocol.CredentialMediationRequirement `json:"mediation,omitempty" msg:"cmr,omitempty"`
7777
}
78+
79+
// GetRelyingPartyID returns the Relying Party ID the ceremony was begun with, which is the value the rpIdHash in the
80+
// authenticator data must be verified against. The Begin* functions always record it, so it reflects any per-ceremony
81+
// override applied by [WithRegistrationRelyingPartyID] or [WithLoginRelyingPartyID].
82+
//
83+
// The supplied fallback, which callers take from [Config.RPID], is returned when the session carries no Relying Party
84+
// ID. That is the case for a session encoded before this member existed, and for one a caller constructed directly,
85+
// neither of which can have used a per-ceremony override.
86+
func (s SessionData) GetRelyingPartyID(fallback string) string {
87+
if len(s.RelyingPartyID) == 0 {
88+
return fallback
89+
}
90+
91+
return s.RelyingPartyID
92+
}

webauthn/types_session_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,39 @@ import (
1414
"github.com/go-webauthn/webauthn/protocol/webauthncose"
1515
)
1616

17+
func TestSessionData_GetRelyingPartyID(t *testing.T) {
18+
testCases := []struct {
19+
name string
20+
session SessionData
21+
fallback string
22+
expected string
23+
}{
24+
{
25+
name: "ShouldUseSessionValue",
26+
session: SessionData{RelyingPartyID: "a.example.com"},
27+
fallback: "example.com",
28+
expected: "a.example.com",
29+
},
30+
{
31+
name: "ShouldFallBackWhenSessionValueIsEmpty",
32+
session: SessionData{},
33+
fallback: "example.com",
34+
expected: "example.com",
35+
},
36+
{
37+
name: "ShouldFallBackToEmptyWhenNeitherIsSet",
38+
session: SessionData{},
39+
expected: "",
40+
},
41+
}
42+
43+
for _, tc := range testCases {
44+
t.Run(tc.name, func(t *testing.T) {
45+
assert.Equal(t, tc.expected, tc.session.GetRelyingPartyID(tc.fallback))
46+
})
47+
}
48+
}
49+
1750
func TestSessionData_MsgpRoundTrip(t *testing.T) {
1851
original := newPopulatedSessionData()
1952

0 commit comments

Comments
 (0)